|
1 | | -// Claim lifecycle and DAO voting will be implemented here. |
2 | | -// |
3 | | -// Planned public functions: |
4 | | -// file_claim(env, policy_id, amount, details, image_urls) |
5 | | -// vote_on_claim(env, voter, claim_id, vote) |
| 1 | +use crate::{ |
| 2 | + storage, |
| 3 | + types::{Claim, ClaimProcessed, ClaimStatus}, |
| 4 | + validate::Error, |
| 5 | +}; |
| 6 | +use soroban_sdk::{symbol_short, token, Address, Env}; |
| 7 | + |
| 8 | +pub fn process_claim(env: &Env, claim_id: u64) -> Result<(), Error> { |
| 9 | + let mut claim = storage::get_claim(env, claim_id).ok_or(Error::ClaimNotFound)?; |
| 10 | + |
| 11 | + if claim.status == ClaimStatus::Paid { |
| 12 | + return Err(Error::AlreadyPaid); |
| 13 | + } |
| 14 | + if claim.status != ClaimStatus::Approved { |
| 15 | + return Err(Error::ClaimNotApproved); |
| 16 | + } |
| 17 | + if claim.amount <= 0 { |
| 18 | + return Err(Error::ClaimAmountZero); |
| 19 | + } |
| 20 | + if !is_allowed_asset(env, &claim.asset) { |
| 21 | + return Err(Error::InvalidAsset); |
| 22 | + } |
| 23 | + |
| 24 | + let token_client = token::Client::new(env, &claim.asset); |
| 25 | + let treasury = treasury_address(env); |
| 26 | + check_treasury_balance(&token_client, &treasury, claim.amount)?; |
| 27 | + |
| 28 | + token_client.transfer(&treasury, &claim.claimant, &claim.amount); |
| 29 | + |
| 30 | + claim.status = ClaimStatus::Paid; |
| 31 | + claim.paid_at = Some(env.ledger().timestamp()); |
| 32 | + storage::set_claim(env, &claim); |
| 33 | + emit_claim_processed(env, &claim); |
| 34 | + |
| 35 | + Ok(()) |
| 36 | +} |
| 37 | + |
| 38 | +pub fn get_claim(env: &Env, claim_id: u64) -> Result<Claim, Error> { |
| 39 | + storage::get_claim(env, claim_id).ok_or(Error::ClaimNotFound) |
| 40 | +} |
| 41 | + |
| 42 | +pub fn is_allowed_asset(env: &Env, asset: &Address) -> bool { |
| 43 | + storage::is_allowed_asset(env, asset) |
| 44 | +} |
| 45 | + |
| 46 | +pub fn set_allowed_asset(env: &Env, asset: &Address, allowed: bool) { |
| 47 | + storage::set_allowed_asset(env, asset, allowed); |
| 48 | +} |
| 49 | + |
| 50 | +pub fn treasury_address(env: &Env) -> Address { |
| 51 | + env.current_contract_address() |
| 52 | +} |
| 53 | + |
| 54 | +fn check_treasury_balance( |
| 55 | + token_client: &token::Client, |
| 56 | + treasury: &Address, |
| 57 | + amount: i128, |
| 58 | +) -> Result<(), Error> { |
| 59 | + if token_client.balance(treasury) < amount { |
| 60 | + return Err(Error::InsufficientTreasury); |
| 61 | + } |
| 62 | + Ok(()) |
| 63 | +} |
| 64 | + |
| 65 | +fn emit_claim_processed(env: &Env, claim: &Claim) { |
| 66 | + env.events().publish( |
| 67 | + (symbol_short!("claim_paid"), claim.claim_id), |
| 68 | + ClaimProcessed { |
| 69 | + claim_id: claim.claim_id, |
| 70 | + recipient: claim.claimant.clone(), |
| 71 | + amount: claim.amount, |
| 72 | + asset: claim.asset.clone(), |
| 73 | + }, |
| 74 | + ); |
| 75 | +} |
| 76 | + |
| 77 | +#[cfg(test)] |
| 78 | +mod tests { |
| 79 | + use super::*; |
| 80 | + use crate::NiffyInsureClient; |
| 81 | + use soroban_sdk::{testutils::Address as _, token, Address, Env, String, Vec}; |
| 82 | + |
| 83 | + fn setup() -> ( |
| 84 | + Env, |
| 85 | + NiffyInsureClient, |
| 86 | + Address, |
| 87 | + token::Client, |
| 88 | + token::StellarAssetClient, |
| 89 | + Address, |
| 90 | + ) { |
| 91 | + let env = Env::default(); |
| 92 | + env.mock_all_auths(); |
| 93 | + |
| 94 | + let contract_id = env.register(crate::NiffyInsure, ()); |
| 95 | + let client = NiffyInsureClient::new(&env, &contract_id); |
| 96 | + |
| 97 | + let admin = Address::generate(&env); |
| 98 | + let token_admin = Address::generate(&env); |
| 99 | + let token_id = env.register_stellar_asset_contract_v2(token_admin.clone()).address(); |
| 100 | + let token_client = token::Client::new(&env, &token_id); |
| 101 | + let token_admin_client = token::StellarAssetClient::new(&env, &token_id); |
| 102 | + |
| 103 | + client.initialize(&admin, &token_id); |
| 104 | + |
| 105 | + ( |
| 106 | + env, |
| 107 | + client, |
| 108 | + contract_id, |
| 109 | + token_client, |
| 110 | + token_admin_client, |
| 111 | + token_id, |
| 112 | + ) |
| 113 | + } |
| 114 | + |
| 115 | + fn approved_claim( |
| 116 | + env: &Env, |
| 117 | + claimant: &Address, |
| 118 | + asset: &Address, |
| 119 | + amount: i128, |
| 120 | + ) -> Claim { |
| 121 | + Claim { |
| 122 | + claim_id: 1, |
| 123 | + policy_id: 7, |
| 124 | + claimant: claimant.clone(), |
| 125 | + amount, |
| 126 | + asset: asset.clone(), |
| 127 | + details: String::from_str(env, "approved fire claim"), |
| 128 | + image_urls: Vec::new(env), |
| 129 | + status: ClaimStatus::Approved, |
| 130 | + approve_votes: 2, |
| 131 | + reject_votes: 0, |
| 132 | + paid_at: None, |
| 133 | + } |
| 134 | + } |
| 135 | + |
| 136 | + #[test] |
| 137 | + fn process_claim_transfers_tokens_and_marks_claim_paid() { |
| 138 | + let (env, client, contract_id, token_client, token_admin_client, token_id) = setup(); |
| 139 | + let claimant = Address::generate(&env); |
| 140 | + let treasury = contract_id.clone(); |
| 141 | + let claim = approved_claim(&env, &claimant, &token_id, 5_000); |
| 142 | + |
| 143 | + token_admin_client.mint(&treasury, &10_000); |
| 144 | + storage::set_claim(&env, &claim); |
| 145 | + |
| 146 | + let before_events = env.events().all().len(); |
| 147 | + client.process_claim(&claim.claim_id); |
| 148 | + |
| 149 | + let stored = client.get_claim(&claim.claim_id); |
| 150 | + assert_eq!(token_client.balance(&treasury), 5_000); |
| 151 | + assert_eq!(token_client.balance(&claimant), 5_000); |
| 152 | + assert_eq!(stored.status, ClaimStatus::Paid); |
| 153 | + assert!(stored.paid_at.is_some()); |
| 154 | + assert!(env.events().all().len() > before_events); |
| 155 | + } |
| 156 | + |
| 157 | + #[test] |
| 158 | + fn process_claim_reverts_when_treasury_is_short() { |
| 159 | + let (env, client, contract_id, token_client, token_admin_client, token_id) = setup(); |
| 160 | + let claimant = Address::generate(&env); |
| 161 | + let treasury = contract_id.clone(); |
| 162 | + let claim = approved_claim(&env, &claimant, &token_id, 5_000); |
| 163 | + |
| 164 | + token_admin_client.mint(&treasury, &1_000); |
| 165 | + storage::set_claim(&env, &claim); |
| 166 | + |
| 167 | + let result = client.try_process_claim(&claim.claim_id); |
| 168 | + assert!(result.is_err()); |
| 169 | + |
| 170 | + let stored = client.get_claim(&claim.claim_id); |
| 171 | + assert_eq!(stored.status, ClaimStatus::Approved); |
| 172 | + assert_eq!(stored.paid_at, None); |
| 173 | + assert_eq!(token_client.balance(&treasury), 1_000); |
| 174 | + assert_eq!(token_client.balance(&claimant), 0); |
| 175 | + } |
| 176 | + |
| 177 | + #[test] |
| 178 | + fn process_claim_is_idempotent() { |
| 179 | + let (env, client, contract_id, _token_client, token_admin_client, token_id) = setup(); |
| 180 | + let claimant = Address::generate(&env); |
| 181 | + let treasury = contract_id.clone(); |
| 182 | + let claim = approved_claim(&env, &claimant, &token_id, 5_000); |
| 183 | + |
| 184 | + token_admin_client.mint(&treasury, &10_000); |
| 185 | + storage::set_claim(&env, &claim); |
| 186 | + |
| 187 | + client.process_claim(&claim.claim_id); |
| 188 | + let second = client.try_process_claim(&claim.claim_id); |
| 189 | + assert!(second.is_err()); |
| 190 | + assert_eq!(client.get_claim(&claim.claim_id).status, ClaimStatus::Paid); |
| 191 | + } |
| 192 | + |
| 193 | + #[test] |
| 194 | + fn process_claim_rejects_assets_outside_the_allowlist() { |
| 195 | + let (env, client, contract_id, token_client, token_admin_client, _token_id) = setup(); |
| 196 | + let claimant = Address::generate(&env); |
| 197 | + let treasury = contract_id.clone(); |
| 198 | + |
| 199 | + let other_admin = Address::generate(&env); |
| 200 | + let other_asset = env |
| 201 | + .register_stellar_asset_contract_v2(other_admin.clone()) |
| 202 | + .address(); |
| 203 | + let claim = approved_claim(&env, &claimant, &other_asset, 5_000); |
| 204 | + |
| 205 | + token_admin_client.mint(&treasury, &10_000); |
| 206 | + storage::set_claim(&env, &claim); |
| 207 | + |
| 208 | + let result = client.try_process_claim(&claim.claim_id); |
| 209 | + assert!(result.is_err()); |
| 210 | + assert_eq!(client.get_claim(&claim.claim_id).status, ClaimStatus::Approved); |
| 211 | + assert_eq!(token_client.balance(&treasury), 10_000); |
| 212 | + } |
| 213 | +} |
0 commit comments