Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
71 changes: 71 additions & 0 deletions contracts/niffyinsure/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -207,6 +207,41 @@ impl NiffyInsure {
storage::get_claim_counter(&env)
}

/// Paginated listing of claims by claim_id range, ordered ascending.
///
/// `start_after` is an exclusive cursor: pass `0` for the first page, or the
/// last `claim_id` received to advance to the next page.
/// `limit` is capped at `PAGE_SIZE_MAX` (20); larger values are silently clamped.
///
/// Returns summary structs — call `get_claim` for the full record.
///
/// Empty page (len == 0) means no more results exist beyond the cursor.
/// Because claim_ids are monotonically increasing and never deleted, a
/// stale cursor never panics — it simply returns an empty page.
pub fn list_claims(
env: Env,
start_after: u64,
limit: u32,
) -> Vec<types::ClaimSummary> {
let cap = limit.min(types::PAGE_SIZE_MAX);
let total = storage::get_claim_counter(&env);
let mut results: Vec<types::ClaimSummary> = Vec::new(&env);
let mut id: u64 = start_after.saturating_add(1);
while id <= total && results.len() < cap {
if let Some(c) = storage::get_claim(&env, id) {
results.push_back(types::ClaimSummary {
claim_id: c.claim_id,
policy_id: c.policy_id,
amount: c.amount,
status: c.status,
filed_at: c.filed_at,
});
}
id = id.saturating_add(1);
}
results
}

pub fn get_policy_counter(env: Env, holder: Address) -> u32 {
storage::get_policy_counter(&env, &holder)
}
Expand Down Expand Up @@ -284,6 +319,42 @@ impl NiffyInsure {
storage::get_policy(&env, &holder, policy_id)
}

/// Paginated listing of a holder's policies, ordered by ascending policy_id.
///
/// `start_after` is an exclusive cursor: pass `0` for the first page, or the
/// last `policy_id` received to advance to the next page.
/// `limit` is capped at `PAGE_SIZE_MAX` (20); larger values are silently clamped.
///
/// Returns summary structs — call `get_policy` for the full record.
///
/// Empty page (len == 0) means no more results exist beyond the cursor.
/// Because policy_ids are monotonically increasing and never deleted, a
/// stale cursor never panics — it simply returns an empty page.
pub fn list_policies(
env: Env,
holder: Address,
start_after: u32,
limit: u32,
) -> Vec<types::PolicySummary> {
let cap = limit.min(types::PAGE_SIZE_MAX);
let total = storage::get_policy_counter(&env, &holder);
let mut results: Vec<types::PolicySummary> = Vec::new(&env);
let mut id: u32 = start_after.saturating_add(1);
while id <= total && results.len() < cap {
if let Some(p) = storage::get_policy(&env, &holder, id) {
results.push_back(types::PolicySummary {
policy_id: p.policy_id,
policy_type: p.policy_type,
coverage: p.coverage,
is_active: p.is_active,
end_ledger: p.end_ledger,
});
}
id = id.saturating_add(1);
}
results
}

/// Read-only: number of active policies for a holder (= vote weight).
pub fn get_active_policy_count(env: Env, holder: Address) -> u32 {
storage::get_active_policy_count(&env, &holder)
Expand Down
48 changes: 48 additions & 0 deletions contracts/niffyinsure/src/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -173,6 +173,54 @@ pub enum TerminationReason {
ExcessiveRejections,
}

// ── Pagination ────────────────────────────────────────────────────────────────

/// Hard cap on items returned per paginated call.
///
/// Soroban charges per-entry read fees; returning more than this in a single
/// simulation would blow the default instruction budget. Callers requesting
/// a larger `limit` receive exactly `PAGE_SIZE_MAX` items — never an error.
///
/// Ordering: policies are returned in ascending `policy_id` order; claims in
/// ascending `claim_id` order. Both orderings are stable across calls as long
/// as no items are deleted (items are never deleted in this contract).
///
/// Stale-cursor note: cursors are plain integer offsets (policy_id / claim_id).
/// If the underlying counter has not changed between pages, the cursor is safe.
/// Because IDs are monotonically increasing and records are never deleted,
/// a cursor pointing past the last item simply returns an empty page — it
/// never panics or skips records.
pub const PAGE_SIZE_MAX: u32 = 20;

/// Lightweight policy summary returned by `list_policies`.
///
/// Omits large or rarely-needed fields (`details`, `image_urls`, etc.) to keep
/// per-page byte cost predictable. Callers that need the full record should
/// follow up with `get_policy(holder, policy_id)`.
#[contracttype]
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct PolicySummary {
pub policy_id: u32,
pub policy_type: PolicyType,
pub coverage: i128,
pub is_active: bool,
pub end_ledger: u32,
}

/// Lightweight claim summary returned by `list_claims`.
///
/// Omits `details` and `image_urls` to keep per-page byte cost predictable.
/// Callers that need the full record should follow up with `get_claim(claim_id)`.
#[contracttype]
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct ClaimSummary {
pub claim_id: u64,
pub policy_id: u32,
pub amount: i128,
pub status: ClaimStatus,
pub filed_at: u32,
}

// ── Premium engine structs ────────────────────────────────────────────────────

#[contracttype]
Expand Down
194 changes: 194 additions & 0 deletions contracts/niffyinsure/tests/storage.rs
Original file line number Diff line number Diff line change
Expand Up @@ -328,6 +328,200 @@ fn non_voter_cannot_vote() {
assert!(result.is_err());
}

// ── pagination: list_policies ─────────────────────────────────────────────────

#[test]
fn list_policies_empty_for_new_holder() {
let (env, contract_id, _, _) = setup();
let client = NiffyInsureClient::new(&env, &contract_id);
let holder = Address::generate(&env);
let page = client.list_policies(&holder, &0u32, &10u32);
assert_eq!(page.len(), 0u32);
}

#[test]
fn list_policies_first_page() {
let (env, contract_id, _, token) = setup();
let client = NiffyInsureClient::new(&env, &contract_id);
let holder = Address::generate(&env);

env.as_contract(&contract_id, || {
for id in 1u32..=5 {
storage::set_policy(&env, &holder, id, &make_policy(&holder, id, &token));
env.storage().persistent().set(
&storage::DataKey::PolicyCounter(holder.clone()),
&id,
);
}
});

let page = client.list_policies(&holder, &0u32, &3u32);
assert_eq!(page.len(), 3u32);
assert_eq!(page.get(0).unwrap().policy_id, 1u32);
assert_eq!(page.get(2).unwrap().policy_id, 3u32);
}

#[test]
fn list_policies_second_page_cursor() {
let (env, contract_id, _, token) = setup();
let client = NiffyInsureClient::new(&env, &contract_id);
let holder = Address::generate(&env);

env.as_contract(&contract_id, || {
for id in 1u32..=5 {
storage::set_policy(&env, &holder, id, &make_policy(&holder, id, &token));
env.storage().persistent().set(
&storage::DataKey::PolicyCounter(holder.clone()),
&id,
);
}
});

let page = client.list_policies(&holder, &3u32, &10u32);
assert_eq!(page.len(), 2u32);
assert_eq!(page.get(0).unwrap().policy_id, 4u32);
assert_eq!(page.get(1).unwrap().policy_id, 5u32);
}

#[test]
fn list_policies_cursor_past_end_returns_empty() {
let (env, contract_id, _, token) = setup();
let client = NiffyInsureClient::new(&env, &contract_id);
let holder = Address::generate(&env);

env.as_contract(&contract_id, || {
storage::set_policy(&env, &holder, 1, &make_policy(&holder, 1, &token));
env.storage().persistent().set(
&storage::DataKey::PolicyCounter(holder.clone()),
&1u32,
);
});

let page = client.list_policies(&holder, &99u32, &10u32);
assert_eq!(page.len(), 0u32);
}

#[test]
fn list_policies_limit_clamped_to_page_size_max() {
let (env, contract_id, _, token) = setup();
let client = NiffyInsureClient::new(&env, &contract_id);
let holder = Address::generate(&env);

env.as_contract(&contract_id, || {
for id in 1u32..=25 {
storage::set_policy(&env, &holder, id, &make_policy(&holder, id, &token));
env.storage().persistent().set(
&storage::DataKey::PolicyCounter(holder.clone()),
&id,
);
}
});

let page = client.list_policies(&holder, &0u32, &100u32);
assert_eq!(page.len(), 20u32);
}

// ── pagination: list_claims ───────────────────────────────────────────────────

fn make_claim(env: &Env, claim_id: u64, holder: &Address) -> niffyinsure::types::Claim {
use niffyinsure::types::{Claim, ClaimStatus};
Claim {
claim_id,
policy_id: 1,
claimant: holder.clone(),
amount: 10_000_000,
details: String::from_str(env, "test"),
image_urls: vec![env],
status: ClaimStatus::Processing,
voting_deadline_ledger: 1000,
approve_votes: 0,
reject_votes: 0,
filed_at: 1,
appeal_open_deadline_ledger: 0,
appeals_count: 0,
appeal_deadline_ledger: 0,
appeal_approve_votes: 0,
appeal_reject_votes: 0,
}
}

#[test]
fn list_claims_empty_when_none_filed() {
let (env, contract_id, _, _) = setup();
let client = NiffyInsureClient::new(&env, &contract_id);
let page = client.list_claims(&0u64, &10u32);
assert_eq!(page.len(), 0u32);
}

#[test]
fn list_claims_first_page() {
let (env, contract_id, _, _) = setup();
let client = NiffyInsureClient::new(&env, &contract_id);
let holder = Address::generate(&env);

env.as_contract(&contract_id, || {
for id in 1u64..=5 {
storage::set_claim(&env, &make_claim(&env, id, &holder));
env.storage().instance().set(&storage::DataKey::ClaimCounter, &id);
}
});

let page = client.list_claims(&0u64, &3u32);
assert_eq!(page.len(), 3u32);
assert_eq!(page.get(0).unwrap().claim_id, 1u64);
assert_eq!(page.get(2).unwrap().claim_id, 3u64);
}

#[test]
fn list_claims_last_page_partial() {
let (env, contract_id, _, _) = setup();
let client = NiffyInsureClient::new(&env, &contract_id);
let holder = Address::generate(&env);

env.as_contract(&contract_id, || {
for id in 1u64..=5 {
storage::set_claim(&env, &make_claim(&env, id, &holder));
env.storage().instance().set(&storage::DataKey::ClaimCounter, &id);
}
});

let page = client.list_claims(&4u64, &10u32);
assert_eq!(page.len(), 1u32);
assert_eq!(page.get(0).unwrap().claim_id, 5u64);
}

#[test]
fn list_claims_cursor_past_end_returns_empty() {
let (env, contract_id, _, _) = setup();
let client = NiffyInsureClient::new(&env, &contract_id);
let holder = Address::generate(&env);

env.as_contract(&contract_id, || {
storage::set_claim(&env, &make_claim(&env, 1, &holder));
env.storage().instance().set(&storage::DataKey::ClaimCounter, &1u64);
});

let page = client.list_claims(&999u64, &10u32);
assert_eq!(page.len(), 0u32);
}

#[test]
fn list_claims_oversize_request_clamped() {
let (env, contract_id, _, _) = setup();
let client = NiffyInsureClient::new(&env, &contract_id);
let holder = Address::generate(&env);

env.as_contract(&contract_id, || {
for id in 1u64..=25 {
storage::set_claim(&env, &make_claim(&env, id, &holder));
env.storage().instance().set(&storage::DataKey::ClaimCounter, &id);
}
});

let page = client.list_claims(&0u64, &999u32);
assert_eq!(page.len(), 20u32);
}

// ── counter immutability: generate_premium does not mutate storage ────────────

#[test]
Expand Down
Loading
Loading