Skip to content

Commit d4ddac1

Browse files
authored
Merge pull request #122 from NUMBER72857/feat/claim-payout-14
Feat/claim payout 14
2 parents ec7daac + 9153889 commit d4ddac1

10 files changed

Lines changed: 1161 additions & 295 deletions

File tree

contracts/niffyinsure/src/claim.rs

Lines changed: 213 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,213 @@
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+
}

contracts/niffyinsure/src/lib.rs

Lines changed: 86 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,11 @@
22

33
mod claim;
44
mod policy;
5+
<<<<<<< feat/claim-payout-14
6+
pub mod premium;
7+
=======
58
mod premium;
9+
>>>>>>> main
610
mod storage;
711
mod token;
812
pub mod types;
@@ -15,58 +19,114 @@ pub struct NiffyInsure;
1519

1620
#[contractimpl]
1721
impl NiffyInsure {
18-
/// One-time initialisation: store admin and token contract address.
19-
/// Must be called immediately after deployment.
22+
/// One-time initialisation: store admin and token contract address, and
23+
/// seed the default premium table so quote generation is deterministic.
2024
pub fn initialize(env: Env, admin: Address, token: Address) {
2125
storage::set_admin(&env, &admin);
2226
storage::set_token(&env, &token);
27+
storage::set_multiplier_table(&env, &premium::default_multiplier_table(&env));
28+
storage::set_allowed_asset(&env, &token, true);
2329
}
2430

2531
/// Pure quote path: reads config and computes premium only.
2632
/// This entrypoint intentionally performs no persistent writes.
2733
pub fn generate_premium(
2834
env: Env,
29-
policy_type: types::PolicyType,
30-
region: types::RegionTier,
31-
age: u32,
32-
risk_score: u32,
35+
input: types::RiskInput,
36+
base_amount: i128,
3337
include_breakdown: bool,
34-
) -> Result<types::PremiumQuote, policy::QuoteError> {
35-
policy::generate_premium(
36-
&env,
37-
policy_type,
38-
region,
39-
age,
40-
risk_score,
41-
include_breakdown,
42-
)
43-
}
44-
45-
/// Converts quote failure codes to support-friendly messages for API layers.
38+
) -> Result<types::PremiumQuote, validate::Error> {
39+
policy::generate_premium(&env, input, base_amount, include_breakdown)
40+
}
41+
4642
pub fn quote_error_message(env: Env, code: u32) -> policy::QuoteFailure {
4743
let err = match code {
48-
1 => policy::QuoteError::InvalidAge,
49-
2 => policy::QuoteError::InvalidRiskScore,
50-
3 => policy::QuoteError::InvalidQuoteTtl,
51-
_ => policy::QuoteError::ArithmeticOverflow,
44+
1 => validate::Error::ZeroCoverage,
45+
2 => validate::Error::ZeroPremium,
46+
3 => validate::Error::InvalidLedgerWindow,
47+
4 => validate::Error::PolicyExpired,
48+
5 => validate::Error::PolicyInactive,
49+
6 => validate::Error::ClaimAmountZero,
50+
7 => validate::Error::ClaimExceedsCoverage,
51+
8 => validate::Error::DetailsTooLong,
52+
9 => validate::Error::TooManyImageUrls,
53+
10 => validate::Error::ImageUrlTooLong,
54+
11 => validate::Error::ReasonTooLong,
55+
12 => validate::Error::ClaimAlreadyTerminal,
56+
13 => validate::Error::DuplicateVote,
57+
14 => validate::Error::InvalidBaseAmount,
58+
15 => validate::Error::SafetyScoreOutOfRange,
59+
16 => validate::Error::InvalidConfigVersion,
60+
17 => validate::Error::MissingRegionMultiplier,
61+
18 => validate::Error::MissingAgeMultiplier,
62+
19 => validate::Error::MissingCoverageMultiplier,
63+
20 => validate::Error::RegionMultiplierOutOfBounds,
64+
21 => validate::Error::AgeMultiplierOutOfBounds,
65+
22 => validate::Error::CoverageMultiplierOutOfBounds,
66+
23 => validate::Error::SafetyDiscountOutOfBounds,
67+
24 => validate::Error::Overflow,
68+
25 => validate::Error::DivideByZero,
69+
26 => validate::Error::InvalidQuoteTtl,
70+
27 => validate::Error::NegativePremiumNotSupported,
71+
28 => validate::Error::ClaimNotFound,
72+
29 => validate::Error::InvalidAsset,
73+
30 => validate::Error::InsufficientTreasury,
74+
31 => validate::Error::AlreadyPaid,
75+
_ => validate::Error::ClaimNotApproved,
5276
};
5377
policy::map_quote_error(&env, err)
5478
}
5579

56-
/// Read-only helper for monitoring state in tests / ops tooling.
80+
pub fn update_multiplier_table(
81+
env: Env,
82+
new_table: types::MultiplierTable,
83+
) -> Result<(), validate::Error> {
84+
let admin = storage::get_admin(&env);
85+
admin.require_auth();
86+
premium::update_multiplier_table(&env, &new_table)
87+
}
88+
89+
pub fn get_multiplier_table(env: Env) -> types::MultiplierTable {
90+
storage::get_multiplier_table(&env)
91+
}
92+
93+
pub fn set_allowed_asset(
94+
env: Env,
95+
asset: Address,
96+
allowed: bool,
97+
) {
98+
let admin = storage::get_admin(&env);
99+
admin.require_auth();
100+
claim::set_allowed_asset(&env, &asset, allowed);
101+
}
102+
103+
pub fn is_allowed_asset(env: Env, asset: Address) -> bool {
104+
claim::is_allowed_asset(&env, &asset)
105+
}
106+
107+
pub fn process_claim(env: Env, claim_id: u64) -> Result<(), validate::Error> {
108+
let admin = storage::get_admin(&env);
109+
admin.require_auth();
110+
claim::process_claim(&env, claim_id)
111+
}
112+
113+
pub fn get_claim(env: Env, claim_id: u64) -> Result<types::Claim, validate::Error> {
114+
claim::get_claim(&env, claim_id)
115+
}
116+
57117
pub fn get_claim_counter(env: Env) -> u64 {
58118
storage::get_claim_counter(&env)
59119
}
60120

61-
/// Read-only helper for monitoring state in tests / ops tooling.
62121
pub fn get_policy_counter(env: Env, holder: Address) -> u32 {
63122
storage::get_policy_counter(&env, &holder)
64123
}
65124

66-
/// Read-only helper for monitoring state in tests / ops tooling.
67125
pub fn has_policy(env: Env, holder: Address, policy_id: u32) -> bool {
68126
storage::has_policy(&env, &holder, policy_id)
69127
}
128+
<<<<<<< feat/claim-payout-14
129+
=======
70130

71131
// ── Policy domain ────────────────────────────────────────────────────
72132

@@ -128,4 +188,5 @@ impl NiffyInsure {
128188
// ── Admin / treasury ─────────────────────────────────────────────────
129189
// drain
130190
// implemented in token.rs — issue: feat/admin
191+
>>>>>>> main
131192
}

0 commit comments

Comments
 (0)