Skip to content

Commit 3949e0d

Browse files
committed
feat: fix all error
1 parent 19b17a5 commit 3949e0d

8 files changed

Lines changed: 280 additions & 51 deletions

contracts/niffyinsure/src/claim.rs

Lines changed: 48 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -15,15 +15,20 @@
1515
// - Rejected claims remain visible on-chain for auditability
1616
// - Admin overrides (if implemented) must be documented in governance docs
1717

18+
#![allow(deprecated)]
19+
1820
use crate::{
1921
storage::{self, DataKey},
20-
types::{Claim, ClaimError, ClaimStatus, Policy, VoteOption, DETAILS_MAX_LEN, IMAGE_URLS_MAX, IMAGE_URL_MAX_LEN},
22+
types::{
23+
Claim, ClaimError, ClaimStatus, Policy, VoteOption, DETAILS_MAX_LEN, IMAGE_URLS_MAX,
24+
IMAGE_URL_MAX_LEN,
25+
},
2126
};
2227
use soroban_sdk::{contracttype, Address, Env, String, Vec};
2328

2429
/// Maximum rejected claims before policy is automatically deactivated.
2530
/// This threshold must be aligned with legal review and product specifications.
26-
///
31+
///
2732
/// GOVERNANCE RISK: This constant is hardcoded; changing it requires contract upgrade.
2833
/// Consider making this configurable per-policy-type in future iterations.
2934
pub const MAX_REJECTED_CLAIMS_BEFORE_DEACTIVATION: u32 = 3;
@@ -74,6 +79,27 @@ pub struct ClaimRejected {
7479
pub claimant: Address,
7580
}
7681

82+
/// Emitted when an appeal is opened on a rejected claim.
83+
#[contracttype]
84+
#[derive(Clone, Debug)]
85+
pub struct AppealOpened {
86+
pub claim_id: u64,
87+
pub policy_id: u32,
88+
pub claimant: Address,
89+
pub appeal_number: u32,
90+
pub additional_evidence: String,
91+
}
92+
93+
/// Emitted when an appeal is closed (either approved or finally rejected).
94+
#[contracttype]
95+
#[derive(Clone, Debug)]
96+
pub struct AppealClosed {
97+
pub claim_id: u64,
98+
pub policy_id: u32,
99+
pub claimant: Address,
100+
pub final_status: ClaimStatus,
101+
}
102+
77103
/// Emitted when a policy receives a strike due to claim rejection.
78104
#[contracttype]
79105
#[derive(Clone, Debug)]
@@ -159,6 +185,7 @@ pub fn file_claim(
159185

160186
// Create claim
161187
let claim_id = storage::next_claim_id(env);
188+
let current_ledger = env.ledger().sequence();
162189
let claim = Claim {
163190
claim_id,
164191
policy_id,
@@ -169,6 +196,10 @@ pub fn file_claim(
169196
status: ClaimStatus::Processing,
170197
approve_votes: 0,
171198
reject_votes: 0,
199+
filed_at_ledger: current_ledger,
200+
rejected_at_ledger: None,
201+
appeal_count: 0,
202+
appeal_opened_at_ledger: None,
172203
};
173204

174205
env.storage()
@@ -194,13 +225,15 @@ pub fn file_claim(
194225
/// Casts a vote on a claim.
195226
///
196227
/// Validation:
197-
/// - Claim must exist and be in Processing state
228+
/// - Claim must exist and be in an active voting state (Processing or AppealOpen)
198229
/// - Voter must have an active policy (one-policy-one-vote)
199-
/// - Voter cannot vote twice on the same claim
230+
/// - Voter cannot vote twice on the same claim phase
200231
///
201232
/// State transitions:
202233
/// - Processing → Approved: if approve_votes reaches majority
203234
/// - Processing → Rejected: if reject_votes reaches majority
235+
/// - AppealOpen → Approved: if approve_votes reaches majority
236+
/// - AppealOpen → RejectedFinal: if reject_votes reaches majority
204237
///
205238
/// Rejection consequences:
206239
/// - Increments policy.rejected_claims_count
@@ -212,6 +245,11 @@ pub fn file_claim(
212245
/// - Emits ClaimApproved event
213246
///
214247
/// CRITICAL: Rejection path never transfers payout tokens.
248+
///
249+
/// APPEAL VOTING RULES:
250+
/// - Votes reset when appeal is opened (fresh voting round)
251+
/// - Voters can vote again even if they voted in the initial round
252+
/// - Vote storage key includes claim phase to prevent double-voting within same phase
215253
#[allow(dead_code)]
216254
pub fn vote_on_claim(
217255
env: &Env,
@@ -229,8 +267,8 @@ pub fn vote_on_claim(
229267
.get(&claim_key)
230268
.ok_or(ClaimError::ClaimNotFound)?;
231269

232-
// Validate claim is in Processing state
233-
if claim.status.is_terminal() {
270+
// Validate claim is in an active voting state
271+
if !claim.status.is_voting_active() {
234272
return Err(ClaimError::ClaimAlreadyFinalized);
235273
}
236274

@@ -241,8 +279,9 @@ pub fn vote_on_claim(
241279
return Err(ClaimError::VoterHasNoPolicies);
242280
}
243281

244-
// Check if voter already voted
245-
let vote_key = DataKey::Vote(claim_id, voter.clone());
282+
// Check if voter already voted in this phase
283+
// Vote key includes appeal_count to allow re-voting in appeal phase
284+
let vote_key = DataKey::VotePhase(claim_id, voter.clone(), claim.appeal_count);
246285
if env.storage().persistent().has(&vote_key) {
247286
return Err(ClaimError::AlreadyVoted);
248287
}
@@ -368,10 +407,7 @@ fn finalize_rejection(env: &Env, claim: &mut Claim) -> Result<(), ClaimError> {
368407
// Check if deactivation threshold reached
369408
if policy.rejected_claims_count >= MAX_REJECTED_CLAIMS_BEFORE_DEACTIVATION {
370409
policy.is_active = false;
371-
let reason = String::from_str(
372-
env,
373-
"deactivated: excessive rejected claims",
374-
);
410+
let reason = String::from_str(env, "deactivated: excessive rejected claims");
375411
policy.deactivation_reason = Some(reason.clone());
376412

377413
// Emit deactivation event

contracts/niffyinsure/src/storage.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@ pub enum DataKey {
1111
Claim(u64),
1212
/// (claim_id, voter_address) → VoteOption
1313
Vote(u64, Address),
14+
/// (claim_id, voter_address, appeal_count) → VoteOption for phase-specific voting
15+
VotePhase(u64, Address, u32),
1416
/// Vec<Address> of all current active policyholders (voters)
1517
Voters,
1618
/// Global monotonic claim id counter

contracts/niffyinsure/src/types.rs

Lines changed: 66 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,18 @@ pub const REASON_MAX_LEN: u32 = 128;
2121
/// finalize_claim may be called to settle the outcome.
2222
pub const VOTE_WINDOW_LEDGERS: u32 = 120_960;
2323

24+
/// Maximum number of appeals allowed per claim.
25+
/// Prevents infinite ping-pong of voting rounds.
26+
pub const MAX_APPEALS_PER_CLAIM: u32 = 1;
27+
28+
/// Appeal window in ledgers (~3 days at 5 s/ledger ≈ 51_840 ledgers).
29+
/// Claimant must open appeal within this window after rejection.
30+
pub const APPEAL_WINDOW_LEDGERS: u32 = 51_840;
31+
32+
/// Appeal voting window in ledgers (~5 days at 5 s/ledger ≈ 86_400 ledgers).
33+
/// Shorter than initial vote to expedite resolution.
34+
pub const APPEAL_VOTE_WINDOW_LEDGERS: u32 = 86_400;
35+
2436
// ── policy_id assignment ─────────────────────────────────────────────────────
2537
//
2638
// policy_id is a u32 scoped per holder: the contract increments a per-holder
@@ -48,6 +60,11 @@ pub enum ClaimError {
4860
ClaimAlreadyFinalized = 9,
4961
VoterHasNoPolicies = 10,
5062
AlreadyVoted = 11,
63+
AppealNotAllowed = 12,
64+
AppealWindowExpired = 13,
65+
MaxAppealsReached = 14,
66+
NotClaimant = 15,
67+
AppealNotInProgress = 16,
5168
}
5269

5370
// ── Enums ────────────────────────────────────────────────────────────────────
@@ -72,34 +89,52 @@ pub enum RegionTier {
7289
High, // urban / high-risk zone
7390
}
7491

75-
/// Claim lifecycle state machine.
92+
/// Claim lifecycle state machine with appeals support.
7693
///
7794
/// ```text
7895
/// [filed] → Processing
7996
/// │
8097
/// ┌──────┴──────┐
8198
/// ▼ ▼
82-
/// Approved Rejected
99+
/// Approved Rejected ──┐
100+
/// │ │ (appeal window)
101+
/// ▼ │
102+
/// AppealOpen ◄─┘
103+
/// │
104+
/// ┌──────┴──────┐
105+
/// ▼ ▼
106+
/// Approved RejectedFinal
83107
/// ```
84108
///
85109
/// Transitions:
86-
/// Processing → Approved : majority Approve votes reached
87-
/// Processing → Rejected : majority Reject votes reached OR policy deactivated
110+
/// Processing → Approved : majority Approve votes reached
111+
/// Processing → Rejected : majority Reject votes reached
112+
/// Rejected → AppealOpen : claimant opens appeal within window
113+
/// Rejected → RejectedFinal : appeal window expires without appeal
114+
/// AppealOpen → Approved : majority Approve votes on appeal
115+
/// AppealOpen → RejectedFinal : majority Reject votes on appeal
88116
///
89-
/// Terminal states (Approved / Rejected) are immutable; no re-open path exists
90-
/// on-chain. Off-chain dispute resolution must open a new claim.
117+
/// Terminal states (Approved / RejectedFinal) are immutable.
118+
/// Appeals are capped at MAX_APPEALS_PER_CLAIM to prevent infinite loops.
91119
#[contracttype]
92120
#[derive(Clone, PartialEq, Debug)]
93121
pub enum ClaimStatus {
94122
Processing,
95123
Approved,
96124
Rejected,
125+
AppealOpen,
126+
RejectedFinal,
97127
}
98128

99129
impl ClaimStatus {
100-
/// Returns true only for the two terminal states.
130+
/// Returns true only for the terminal states.
101131
pub fn is_terminal(&self) -> bool {
102-
matches!(self, ClaimStatus::Approved | ClaimStatus::Rejected)
132+
matches!(self, ClaimStatus::Approved | ClaimStatus::RejectedFinal)
133+
}
134+
135+
/// Returns true if the claim is in an active voting phase.
136+
pub fn is_voting_active(&self) -> bool {
137+
matches!(self, ClaimStatus::Processing | ClaimStatus::AppealOpen)
103138
}
104139
}
105140

@@ -155,17 +190,21 @@ pub struct Policy {
155190

156191
/// On-chain claim record.
157192
///
158-
/// | Field | Authoritative | Notes |
159-
/// |---------------|---------------|-------|
160-
/// | claim_id | on-chain | global monotonic u64 from ClaimCounter |
161-
/// | policy_id | on-chain | references Policy(holder, policy_id) |
162-
/// | claimant | on-chain | must equal policy.holder |
163-
/// | amount | on-chain | stroops; 0 < amount ≤ policy.coverage |
164-
/// | details | on-chain | ≤ DETAILS_MAX_LEN bytes |
165-
/// | image_urls | on-chain | ≤ IMAGE_URLS_MAX items, each ≤ IMAGE_URL_MAX_LEN |
166-
/// | status | on-chain | ClaimStatus state machine |
167-
/// | approve_votes | on-chain | running tally |
168-
/// | reject_votes | on-chain | running tally |
193+
/// | Field | Authoritative | Notes |
194+
/// |------------------------|---------------|-------|
195+
/// | claim_id | on-chain | global monotonic u64 from ClaimCounter |
196+
/// | policy_id | on-chain | references Policy(holder, policy_id) |
197+
/// | claimant | on-chain | must equal policy.holder |
198+
/// | amount | on-chain | stroops; 0 < amount ≤ policy.coverage |
199+
/// | details | on-chain | ≤ DETAILS_MAX_LEN bytes |
200+
/// | image_urls | on-chain | ≤ IMAGE_URLS_MAX items, each ≤ IMAGE_URL_MAX_LEN |
201+
/// | status | on-chain | ClaimStatus state machine |
202+
/// | approve_votes | on-chain | running tally |
203+
/// | reject_votes | on-chain | running tally |
204+
/// | filed_at_ledger | on-chain | ledger when claim was filed |
205+
/// | rejected_at_ledger | on-chain | ledger when claim was rejected (for appeal window) |
206+
/// | appeal_count | on-chain | number of appeals opened (capped at MAX_APPEALS_PER_CLAIM) |
207+
/// | appeal_opened_at_ledger| on-chain | ledger when current appeal was opened |
169208
#[contracttype]
170209
#[derive(Clone)]
171210
pub struct Claim {
@@ -181,6 +220,14 @@ pub struct Claim {
181220
pub status: ClaimStatus,
182221
pub approve_votes: u32,
183222
pub reject_votes: u32,
223+
/// Ledger sequence when claim was filed.
224+
pub filed_at_ledger: u32,
225+
/// Ledger sequence when claim was rejected (used for appeal window).
226+
pub rejected_at_ledger: Option<u32>,
227+
/// Number of appeals opened for this claim.
228+
pub appeal_count: u32,
229+
/// Ledger sequence when current appeal was opened.
230+
pub appeal_opened_at_ledger: Option<u32>,
184231
}
185232

186233
/// Premium quote line item for UX display.

0 commit comments

Comments
 (0)