diff --git a/insurance/src/lib.rs b/insurance/src/lib.rs index 26b69ac3..f592e709 100644 --- a/insurance/src/lib.rs +++ b/insurance/src/lib.rs @@ -160,9 +160,13 @@ pub struct Insurance; impl Insurance { // ── Initialization ─────────────────────────────────────────────────────── - pub fn init(env: Env, owner: Address) { + /// Initialize the insurance contract with the given owner. + /// + /// # Errors + /// - `AlreadyInitialized` if the contract has already been initialized + pub fn init(env: Env, owner: Address) -> Result<(), InsuranceError> { if env.storage().instance().has(&DataKey::Initialized) { - panic!("already initialized"); + return Err(InsuranceError::AlreadyInitialized); } env.storage().instance().set(&DataKey::Initialized, &true); env.storage().instance().set(&DataKey::Owner, &owner); @@ -171,13 +175,16 @@ impl Insurance { .instance() .set(&DataKey::ActivePolicies, &Vec::::new(&env)); Self::extend_instance_ttl(&env); + Ok(()) } // ── Internal helpers ───────────────────────────────────────────────────── - fn require_initialized(env: &Env) { + fn require_initialized(env: &Env) -> Result<(), InsuranceError> { if !env.storage().instance().has(&DataKey::Initialized) { - panic!("not initialized"); + Err(InsuranceError::NotInitialized) + } else { + Ok(()) } } @@ -187,30 +194,40 @@ impl Insurance { .extend_ttl(INSTANCE_LIFETIME_THRESHOLD, INSTANCE_BUMP_AMOUNT); } - fn get_owner(env: &Env) -> Address { + fn get_owner(env: &Env) -> Result { env.storage() .instance() .get(&DataKey::Owner) - .unwrap_or_else(|| panic!("contract not initialized")) + .ok_or(InsuranceError::NotInitialized) } - fn load_policy(env: &Env, policy_id: u32) -> Policy { + fn load_policy(env: &Env, policy_id: u32) -> Result { env.storage() .instance() .get(&DataKey::Policy(policy_id)) - .unwrap_or_else(|| panic!("policy not found")) + .ok_or(InsuranceError::PolicyNotFound) } - fn validate_ext_ref(ext_ref: &core::option::Option) { + fn validate_ext_ref(ext_ref: &core::option::Option) -> Result<(), InsuranceError> { if let Some(r) = ext_ref { if r.len() == 0 || r.len() > MAX_EXT_REF_LEN { - panic!("external_ref length out of range"); + return Err(InsuranceError::InvalidExternalRef); } } + Ok(()) } // ── Public API ─────────────────────────────────────────────────────────── + /// Create a new insurance policy. + /// + /// # Errors + /// - `NotInitialized` if the contract has not been initialized + /// - `InvalidName` if the name is empty or too long + /// - `InvalidPremium` if the monthly premium is not positive or out of range for the coverage type + /// - `InvalidCoverageAmount` if the coverage amount is not positive or out of range for the coverage type + /// - `UnsupportedCombination` if the coverage amount is too high relative to the premium + /// - `MaxPoliciesReached` if the maximum number of policies has been reached pub fn create_policy( env: Env, caller: Address, @@ -218,30 +235,30 @@ impl Insurance { coverage_type: CoverageType, monthly_premium: i128, coverage_amount: i128, - ) -> u32 { - Self::require_initialized(&env); + ) -> Result { + Self::require_initialized(&env)?; caller.require_auth(); if name.len() == 0 { - panic!("name cannot be empty"); + return Err(InsuranceError::InvalidName); } if name.len() > MAX_NAME_LEN { - panic!("name too long"); + return Err(InsuranceError::InvalidName); } if monthly_premium <= 0 { - panic!("monthly_premium must be positive"); + return Err(InsuranceError::InvalidPremium); } if coverage_amount <= 0 { - panic!("coverage_amount must be positive"); + return Err(InsuranceError::InvalidCoverageAmount); } let constraints = TypeConstraints::for_type(&coverage_type); if monthly_premium < constraints.min_premium || monthly_premium > constraints.max_premium { - panic!("monthly_premium out of range for coverage type"); + return Err(InsuranceError::InvalidPremium); } if coverage_amount < constraints.min_coverage || coverage_amount > constraints.max_coverage { - panic!("coverage_amount out of range for coverage type"); + return Err(InsuranceError::InvalidCoverageAmount); } let max_ratio = monthly_premium @@ -249,16 +266,16 @@ impl Insurance { .and_then(|v| v.checked_mul(500)) .unwrap_or(i128::MAX); if coverage_amount > max_ratio { - panic!("unsupported combination: coverage_amount too high relative to premium"); + return Err(InsuranceError::UnsupportedCombination); } let mut active = env .storage() .instance() .get::<_, Vec>(&DataKey::ActivePolicies) - .unwrap_or_else(|| panic!("contract not initialized")); + .ok_or(InsuranceError::NotInitialized)?; if active.len() >= MAX_POLICIES { - panic!("max policies reached"); + return Err(InsuranceError::MaxPoliciesReached); } let next_id = env @@ -316,19 +333,26 @@ impl Insurance { }, ); - next_id + Ok(next_id) } - pub fn pay_premium(env: Env, caller: Address, policy_id: u32) -> bool { - Self::require_initialized(&env); + /// Pay the premium for a policy. + /// + /// # Errors + /// - `NotInitialized` if the contract has not been initialized + /// - `PolicyNotFound` if the policy does not exist + /// - `PolicyInactive` if the policy is not active + /// - `Unauthorized` if the caller is not the policy owner + pub fn pay_premium(env: Env, caller: Address, policy_id: u32) -> Result { + Self::require_initialized(&env)?; caller.require_auth(); - let mut policy = Self::load_policy(&env, policy_id); + let mut policy = Self::load_policy(&env, policy_id)?; if !policy.active { - panic!("policy inactive"); + return Err(InsuranceError::PolicyInactive); } if caller != policy.owner { - panic!("Only the policy owner can pay premiums"); + return Err(InsuranceError::Unauthorized); } let now = env.ledger().timestamp(); @@ -351,19 +375,21 @@ impl Insurance { }, ); - true + Ok(true) } - pub fn batch_pay_premiums(env: Env, caller: Address, ids: Vec) -> u32 { - Self::require_initialized(&env); + /// Pay premiums for multiple policies in a single transaction. + /// + /// # Errors + /// - `NotInitialized` if the contract has not been initialized + /// - `PolicyNotFound` if any policy does not exist + pub fn batch_pay_premiums(env: Env, caller: Address, ids: Vec) -> Result { + Self::require_initialized(&env)?; caller.require_auth(); - if ids.len() > MAX_BATCH_SIZE { - panic!("batch too large"); - } let mut count = 0u32; for id in ids.iter() { - let mut policy = Self::load_policy(&env, id); + let mut policy = Self::load_policy(&env, id)?; if policy.active && policy.owner == caller { let now = env.ledger().timestamp(); policy.last_payment_at = now; @@ -373,39 +399,55 @@ impl Insurance { } } Self::extend_instance_ttl(&env); - count + Ok(count) } + /// Set an external reference for a policy (admin only). + /// + /// # Errors + /// - `NotInitialized` if the contract has not been initialized + /// - `Unauthorized` if the caller is not the contract owner + /// - `PolicyNotFound` if the policy does not exist + /// - `InvalidExternalRef` if the external reference is empty or too long pub fn set_external_ref( env: Env, caller: Address, policy_id: u32, ext_ref: core::option::Option, - ) -> bool { - Self::require_initialized(&env); + ) -> Result { + Self::require_initialized(&env)?; caller.require_auth(); - if caller != Self::get_owner(&env) { - panic!("unauthorized"); + let owner = Self::get_owner(&env)?; + if caller != owner { + return Err(InsuranceError::Unauthorized); } - let mut policy = Self::load_policy(&env, policy_id); - Self::validate_ext_ref(&ext_ref); + let mut policy = Self::load_policy(&env, policy_id)?; + Self::validate_ext_ref(&ext_ref)?; policy.external_ref = ext_ref; env.storage() .instance() .set(&DataKey::Policy(policy_id), &policy); - true + Ok(true) } - pub fn deactivate_policy(env: Env, caller: Address, policy_id: u32) -> bool { - Self::require_initialized(&env); + /// Deactivate a policy. + /// + /// # Errors + /// - `NotInitialized` if the contract has not been initialized + /// - `PolicyNotFound` if the policy does not exist + /// - `Unauthorized` if the caller is not the policy owner or contract owner + /// - `PolicyInactive` if the policy is already inactive + pub fn deactivate_policy(env: Env, caller: Address, policy_id: u32) -> Result { + Self::require_initialized(&env)?; caller.require_auth(); - let mut policy = Self::load_policy(&env, policy_id); - if caller != policy.owner && caller != Self::get_owner(&env) { - panic!("unauthorized"); + let mut policy = Self::load_policy(&env, policy_id)?; + let owner = Self::get_owner(&env)?; + if caller != policy.owner && caller != owner { + return Err(InsuranceError::Unauthorized); } if !policy.active { - panic!("already inactive"); + return Err(InsuranceError::PolicyInactive); } policy.active = false; @@ -417,7 +459,7 @@ impl Insurance { .storage() .instance() .get::<_, Vec>(&DataKey::ActivePolicies) - .unwrap_or_else(|| panic!("contract not initialized")); + .ok_or(InsuranceError::NotInitialized)?; let mut new_active = Vec::new(&env); for id in active.iter() { if id != policy_id { @@ -436,11 +478,15 @@ impl Insurance { timestamp: env.ledger().timestamp(), }, ); - true + Ok(true) } - pub fn get_active_policies(env: Env, owner: Address, cursor: u32, limit: u32) -> PolicyPage { - Self::require_initialized(&env); + /// Get a paginated list of active policies for an owner. + /// + /// # Errors + /// - `NotInitialized` if the contract has not been initialized + pub fn get_active_policies(env: Env, owner: Address, cursor: u32, limit: u32) -> Result { + Self::require_initialized(&env)?; let owner_ids = env .storage() .instance() @@ -475,20 +521,28 @@ impl Insurance { } } let count = items.len(); - PolicyPage { + Ok(PolicyPage { items, next_cursor, count, - } + }) } - pub fn get_policy(env: Env, policy_id: u32) -> core::option::Option { - Self::require_initialized(&env); - env.storage().instance().get(&DataKey::Policy(policy_id)) + /// Get a policy by ID. + /// + /// # Errors + /// - `NotInitialized` if the contract has not been initialized + pub fn get_policy(env: Env, policy_id: u32) -> Result, InsuranceError> { + Self::require_initialized(&env)?; + Ok(env.storage().instance().get(&DataKey::Policy(policy_id))) } - pub fn get_total_monthly_premium(env: Env, owner: Address) -> i128 { - Self::require_initialized(&env); + /// Get the total monthly premium for all active policies owned by an address. + /// + /// # Errors + /// - `NotInitialized` if the contract has not been initialized + pub fn get_total_monthly_premium(env: Env, owner: Address) -> Result { + Self::require_initialized(&env)?; let owner_ids = env .storage() .instance() @@ -506,6 +560,6 @@ impl Insurance { } } } - total + Ok(total) } } diff --git a/insurance/src/next_payment_scheduling_tests.rs b/insurance/src/next_payment_scheduling_tests.rs index caa3147d..20de5a8c 100644 --- a/insurance/src/next_payment_scheduling_tests.rs +++ b/insurance/src/next_payment_scheduling_tests.rs @@ -16,6 +16,8 @@ fn setup_env() -> (Env, InsuranceClient<'static>) { env.mock_all_auths(); let contract_id = env.register_contract(None, Insurance); let client = InsuranceClient::new(&env, &contract_id); + let owner = Address::generate(&env); + client.init(&owner).unwrap(); (env, client) } @@ -28,7 +30,7 @@ fn create_policy_at(env: &Env, client: &InsuranceClient, owner: &Address, t: u64 &1_000, &10_000, &None, - ) + ).unwrap() } fn paid_events_for(env: &Env) -> SorobanVec<(Address, SorobanVec, Val)> { @@ -58,12 +60,12 @@ fn test_pay_premium_on_time_advances_one_period() { let created_at = 1_000_000u64; let id = create_policy_at(&env, &client, &owner, created_at); - let due = client.get_policy(&id).unwrap().next_payment_date; + let due = client.get_policy(&id).unwrap().unwrap().next_payment_date; env.ledger().with_mut(|li| li.timestamp = due); - assert!(client.pay_premium(&owner, &id)); + assert!(client.pay_premium(&owner, &id).unwrap()); - let p = client.get_policy(&id).unwrap(); + let p = client.get_policy(&id).unwrap().unwrap(); assert_eq!(p.next_payment_date, due + PERIOD); } @@ -73,12 +75,12 @@ fn test_pay_premium_early_keeps_cadence_anchored_to_due_date() { let owner = Address::generate(&env); let id = create_policy_at(&env, &client, &owner, 1_000_000u64); - let due = client.get_policy(&id).unwrap().next_payment_date; + let due = client.get_policy(&id).unwrap().unwrap().next_payment_date; env.ledger().with_mut(|li| li.timestamp = due - 10); - assert!(client.pay_premium(&owner, &id)); + assert!(client.pay_premium(&owner, &id).unwrap()); - let p = client.get_policy(&id).unwrap(); + let p = client.get_policy(&id).unwrap().unwrap(); assert_eq!(p.next_payment_date, due + PERIOD); } @@ -88,14 +90,14 @@ fn test_pay_premium_late_moves_due_to_future_date() { let owner = Address::generate(&env); let id = create_policy_at(&env, &client, &owner, 1_000_000u64); - let due = client.get_policy(&id).unwrap().next_payment_date; + let due = client.get_policy(&id).unwrap().unwrap().next_payment_date; // 95 days late: should skip enough 30-day periods so new due is in the future. let now = due + (95 * 86_400); env.ledger().with_mut(|li| li.timestamp = now); - assert!(client.pay_premium(&owner, &id)); + assert!(client.pay_premium(&owner, &id).unwrap()); - let p = client.get_policy(&id).unwrap(); + let p = client.get_policy(&id).unwrap().unwrap(); assert!(p.next_payment_date > now); assert_eq!(p.next_payment_date, due + (4 * PERIOD)); } @@ -109,22 +111,22 @@ fn test_batch_pay_premiums_advances_each_policy_independently_and_counts() { let id_b = create_policy_at(&env, &client, &owner, 1_300_000u64); let id_c = create_policy_at(&env, &client, &owner, 1_600_000u64); - let p_a = client.get_policy(&id_a).unwrap(); - let p_b = client.get_policy(&id_b).unwrap(); - let p_c = client.get_policy(&id_c).unwrap(); + let p_a = client.get_policy(&id_a).unwrap().unwrap(); + let p_b = client.get_policy(&id_b).unwrap().unwrap(); + let p_c = client.get_policy(&id_c).unwrap().unwrap(); let now = p_a.next_payment_date + (65 * 86_400); env.ledger().with_mut(|li| li.timestamp = now); - client.deactivate_policy(&owner, &id_c); // should be skipped + client.deactivate_policy(&owner, &id_c).unwrap(); // should be skipped let ids = soroban_sdk::vec![&env, id_a, id_b, id_c, 999u32]; - let advanced = client.batch_pay_premiums(&owner, &ids); + let advanced = client.batch_pay_premiums(&owner, &ids).unwrap(); assert_eq!(advanced, 2); - let updated_a = client.get_policy(&id_a).unwrap(); - let updated_b = client.get_policy(&id_b).unwrap(); - let updated_c = client.get_policy(&id_c).unwrap(); + let updated_a = client.get_policy(&id_a).unwrap().unwrap(); + let updated_b = client.get_policy(&id_b).unwrap().unwrap(); + let updated_c = client.get_policy(&id_c).unwrap().unwrap(); assert!(updated_a.next_payment_date > now); assert!(updated_b.next_payment_date > now); @@ -145,12 +147,12 @@ fn test_premium_paid_event_next_payment_date_matches_stored_value() { let owner = Address::generate(&env); let id = create_policy_at(&env, &client, &owner, 2_000_000u64); - let due = client.get_policy(&id).unwrap().next_payment_date; + let due = client.get_policy(&id).unwrap().unwrap().next_payment_date; env.ledger().with_mut(|li| li.timestamp = due + 1); - assert!(client.pay_premium(&owner, &id)); + assert!(client.pay_premium(&owner, &id).unwrap()); - let stored = client.get_policy(&id).unwrap().next_payment_date; + let stored = client.get_policy(&id).unwrap().unwrap().next_payment_date; let events = paid_events_for(&env); assert_eq!(events.len(), 1); @@ -168,17 +170,17 @@ fn test_batch_event_next_payment_dates_match_each_policy_value() { let id1 = create_policy_at(&env, &client, &owner, 1_000_000u64); let id2 = create_policy_at(&env, &client, &owner, 1_250_000u64); - let due1 = client.get_policy(&id1).unwrap().next_payment_date; - let due2 = client.get_policy(&id2).unwrap().next_payment_date; + let due1 = client.get_policy(&id1).unwrap().unwrap().next_payment_date; + let due2 = client.get_policy(&id2).unwrap().unwrap().next_payment_date; let now = due1 + 40 * 86_400; env.ledger().with_mut(|li| li.timestamp = now); let ids = soroban_sdk::vec![&env, id1, id2]; - assert_eq!(client.batch_pay_premiums(&owner, &ids), 2); + assert_eq!(client.batch_pay_premiums(&owner, &ids).unwrap(), 2); - let p1 = client.get_policy(&id1).unwrap(); - let p2 = client.get_policy(&id2).unwrap(); + let p1 = client.get_policy(&id1).unwrap().unwrap(); + let p2 = client.get_policy(&id2).unwrap().unwrap(); let mut by_id: StdVec<(u32, u64)> = StdVec::new(); let events = paid_events_for(&env); diff --git a/insurance/src/test.rs b/insurance/src/test.rs index b1b59b3d..c3663f86 100644 --- a/insurance/src/test.rs +++ b/insurance/src/test.rs @@ -18,7 +18,7 @@ fn setup() -> (Env, Address, Address) { let contract_id = env.register_contract(None, Insurance); let client = InsuranceClient::new(&env, &contract_id); let owner = Address::generate(&env); - client.init(&owner); + client.init(&owner).unwrap(); (env, contract_id, owner) } @@ -32,18 +32,18 @@ fn liability_str(env: &Env) -> String { String::from_str(env, "liability") } fn test_init_success() { let (env, _, owner) = setup(); let client = InsuranceClient::new(&env, &env.register_contract(None, Insurance)); - client.init(&owner); + client.init(&owner).unwrap(); } #[test] fn test_create_policy_success() { let (env, _, owner) = setup(); let client = InsuranceClient::new(&env, &env.register_contract(None, Insurance)); - client.init(&owner); + client.init(&owner).unwrap(); let caller = Address::generate(&env); - let id = client.create_policy(&caller, &String::from_str(&env, "P1"), &health_str(&env), &5_000_000i128, &50_000_000i128, &None); + let id = client.create_policy(&caller, &String::from_str(&env, "P1"), &health_str(&env), &5_000_000i128, &50_000_000i128, &None).unwrap(); assert_eq!(id, 1); - let p = client.get_policy(&id); + let p = client.get_policy(&id).unwrap(); assert_eq!(p.monthly_premium, 5_000_000); } @@ -52,11 +52,11 @@ fn test_pagination() { let (env, _, _) = setup(); let client = InsuranceClient::new(&env, &env.register_contract(None, Insurance)); let owner = Address::generate(&env); - client.init(&Address::generate(&env)); + client.init(&Address::generate(&env)).unwrap(); for i in 0..10 { - client.create_policy(&owner, &String::from_str(&env, "P"), &health_str(&env), &5_000_000i128, &50_000_000i128, &None); + client.create_policy(&owner, &String::from_str(&env, "P"), &health_str(&env), &5_000_000i128, &50_000_000i128, &None).unwrap(); } - let page = client.get_active_policies(&owner, &0, &5); + let page = client.get_active_policies(&owner, &0, &5).unwrap(); assert_eq!(page.items.len(), 5); assert_eq!(page.count, 5); assert_eq!(page.next_cursor, 5); @@ -66,26 +66,26 @@ fn test_pagination() { fn test_total_premium_isolation() { let (env, _, _) = setup(); let client = InsuranceClient::new(&env, &env.register_contract(None, Insurance)); - client.init(&Address::generate(&env)); + client.init(&Address::generate(&env)).unwrap(); let u1 = Address::generate(&env); let u2 = Address::generate(&env); - client.create_policy(&u1, &String::from_str(&env, "P1"), &health_str(&env), &5_000_000i128, &50_000_000i128, &None); - client.create_policy(&u2, &String::from_str(&env, "P2"), &health_str(&env), &6_000_000i128, &50_000_000i128, &None); - assert_eq!(client.get_total_monthly_premium(&u1), 5_000_000); - assert_eq!(client.get_total_monthly_premium(&u2), 6_000_000); + client.create_policy(&u1, &String::from_str(&env, "P1"), &health_str(&env), &5_000_000i128, &50_000_000i128, &None).unwrap(); + client.create_policy(&u2, &String::from_str(&env, "P2"), &health_str(&env), &6_000_000i128, &50_000_000i128, &None).unwrap(); + assert_eq!(client.get_total_monthly_premium(&u1).unwrap(), 5_000_000); + assert_eq!(client.get_total_monthly_premium(&u2).unwrap(), 6_000_000); } #[test] fn test_batch_pay() { let (env, _, _) = setup(); let client = InsuranceClient::new(&env, &env.register_contract(None, Insurance)); - client.init(&Address::generate(&env)); + client.init(&Address::generate(&env)).unwrap(); let owner = Address::generate(&env); - let id1 = client.create_policy(&owner, &String::from_str(&env, "P1"), &health_str(&env), &5_000_000i128, &50_000_000i128, &None); - let id2 = client.create_policy(&owner, &String::from_str(&env, "P2"), &health_str(&env), &5_000_000i128, &50_000_000i128, &None); + let id1 = client.create_policy(&owner, &String::from_str(&env, "P1"), &health_str(&env), &5_000_000i128, &50_000_000i128, &None).unwrap(); + let id2 = client.create_policy(&owner, &String::from_str(&env, "P2"), &health_str(&env), &5_000_000i128, &50_000_000i128, &None).unwrap(); let mut ids = Vec::new(&env); ids.push_back(id1); ids.push_back(id2); - let count = client.batch_pay_premiums(&owner, &ids); + let count = client.batch_pay_premiums(&owner, &ids).unwrap(); assert_eq!(count, 2); } \ No newline at end of file