Skip to content

Commit 0a5c2b2

Browse files
committed
feat: Read/query entrypoints: get_policy, get_claim, bounded pagination
1 parent 3970e7e commit 0a5c2b2

5 files changed

Lines changed: 481 additions & 0 deletions

File tree

contracts/niffyinsure/src/lib.rs

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -207,6 +207,41 @@ impl NiffyInsure {
207207
storage::get_claim_counter(&env)
208208
}
209209

210+
/// Paginated listing of claims by claim_id range, ordered ascending.
211+
///
212+
/// `start_after` is an exclusive cursor: pass `0` for the first page, or the
213+
/// last `claim_id` received to advance to the next page.
214+
/// `limit` is capped at `PAGE_SIZE_MAX` (20); larger values are silently clamped.
215+
///
216+
/// Returns summary structs — call `get_claim` for the full record.
217+
///
218+
/// Empty page (len == 0) means no more results exist beyond the cursor.
219+
/// Because claim_ids are monotonically increasing and never deleted, a
220+
/// stale cursor never panics — it simply returns an empty page.
221+
pub fn list_claims(
222+
env: Env,
223+
start_after: u64,
224+
limit: u32,
225+
) -> Vec<types::ClaimSummary> {
226+
let cap = limit.min(types::PAGE_SIZE_MAX);
227+
let total = storage::get_claim_counter(&env);
228+
let mut results: Vec<types::ClaimSummary> = Vec::new(&env);
229+
let mut id: u64 = start_after.saturating_add(1);
230+
while id <= total && results.len() < cap {
231+
if let Some(c) = storage::get_claim(&env, id) {
232+
results.push_back(types::ClaimSummary {
233+
claim_id: c.claim_id,
234+
policy_id: c.policy_id,
235+
amount: c.amount,
236+
status: c.status,
237+
filed_at: c.filed_at,
238+
});
239+
}
240+
id = id.saturating_add(1);
241+
}
242+
results
243+
}
244+
210245
pub fn get_policy_counter(env: Env, holder: Address) -> u32 {
211246
storage::get_policy_counter(&env, &holder)
212247
}
@@ -284,6 +319,42 @@ impl NiffyInsure {
284319
storage::get_policy(&env, &holder, policy_id)
285320
}
286321

322+
/// Paginated listing of a holder's policies, ordered by ascending policy_id.
323+
///
324+
/// `start_after` is an exclusive cursor: pass `0` for the first page, or the
325+
/// last `policy_id` received to advance to the next page.
326+
/// `limit` is capped at `PAGE_SIZE_MAX` (20); larger values are silently clamped.
327+
///
328+
/// Returns summary structs — call `get_policy` for the full record.
329+
///
330+
/// Empty page (len == 0) means no more results exist beyond the cursor.
331+
/// Because policy_ids are monotonically increasing and never deleted, a
332+
/// stale cursor never panics — it simply returns an empty page.
333+
pub fn list_policies(
334+
env: Env,
335+
holder: Address,
336+
start_after: u32,
337+
limit: u32,
338+
) -> Vec<types::PolicySummary> {
339+
let cap = limit.min(types::PAGE_SIZE_MAX);
340+
let total = storage::get_policy_counter(&env, &holder);
341+
let mut results: Vec<types::PolicySummary> = Vec::new(&env);
342+
let mut id: u32 = start_after.saturating_add(1);
343+
while id <= total && results.len() < cap {
344+
if let Some(p) = storage::get_policy(&env, &holder, id) {
345+
results.push_back(types::PolicySummary {
346+
policy_id: p.policy_id,
347+
policy_type: p.policy_type,
348+
coverage: p.coverage,
349+
is_active: p.is_active,
350+
end_ledger: p.end_ledger,
351+
});
352+
}
353+
id = id.saturating_add(1);
354+
}
355+
results
356+
}
357+
287358
/// Read-only: number of active policies for a holder (= vote weight).
288359
pub fn get_active_policy_count(env: Env, holder: Address) -> u32 {
289360
storage::get_active_policy_count(&env, &holder)

contracts/niffyinsure/src/types.rs

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -173,6 +173,54 @@ pub enum TerminationReason {
173173
ExcessiveRejections,
174174
}
175175

176+
// ── Pagination ────────────────────────────────────────────────────────────────
177+
178+
/// Hard cap on items returned per paginated call.
179+
///
180+
/// Soroban charges per-entry read fees; returning more than this in a single
181+
/// simulation would blow the default instruction budget. Callers requesting
182+
/// a larger `limit` receive exactly `PAGE_SIZE_MAX` items — never an error.
183+
///
184+
/// Ordering: policies are returned in ascending `policy_id` order; claims in
185+
/// ascending `claim_id` order. Both orderings are stable across calls as long
186+
/// as no items are deleted (items are never deleted in this contract).
187+
///
188+
/// Stale-cursor note: cursors are plain integer offsets (policy_id / claim_id).
189+
/// If the underlying counter has not changed between pages, the cursor is safe.
190+
/// Because IDs are monotonically increasing and records are never deleted,
191+
/// a cursor pointing past the last item simply returns an empty page — it
192+
/// never panics or skips records.
193+
pub const PAGE_SIZE_MAX: u32 = 20;
194+
195+
/// Lightweight policy summary returned by `list_policies`.
196+
///
197+
/// Omits large or rarely-needed fields (`details`, `image_urls`, etc.) to keep
198+
/// per-page byte cost predictable. Callers that need the full record should
199+
/// follow up with `get_policy(holder, policy_id)`.
200+
#[contracttype]
201+
#[derive(Clone, Debug, Eq, PartialEq)]
202+
pub struct PolicySummary {
203+
pub policy_id: u32,
204+
pub policy_type: PolicyType,
205+
pub coverage: i128,
206+
pub is_active: bool,
207+
pub end_ledger: u32,
208+
}
209+
210+
/// Lightweight claim summary returned by `list_claims`.
211+
///
212+
/// Omits `details` and `image_urls` to keep per-page byte cost predictable.
213+
/// Callers that need the full record should follow up with `get_claim(claim_id)`.
214+
#[contracttype]
215+
#[derive(Clone, Debug, Eq, PartialEq)]
216+
pub struct ClaimSummary {
217+
pub claim_id: u64,
218+
pub policy_id: u32,
219+
pub amount: i128,
220+
pub status: ClaimStatus,
221+
pub filed_at: u32,
222+
}
223+
176224
// ── Premium engine structs ────────────────────────────────────────────────────
177225

178226
#[contracttype]

contracts/niffyinsure/tests/storage.rs

Lines changed: 194 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -328,6 +328,200 @@ fn non_voter_cannot_vote() {
328328
assert!(result.is_err());
329329
}
330330

331+
// ── pagination: list_policies ─────────────────────────────────────────────────
332+
333+
#[test]
334+
fn list_policies_empty_for_new_holder() {
335+
let (env, contract_id, _, _) = setup();
336+
let client = NiffyInsureClient::new(&env, &contract_id);
337+
let holder = Address::generate(&env);
338+
let page = client.list_policies(&holder, &0u32, &10u32);
339+
assert_eq!(page.len(), 0u32);
340+
}
341+
342+
#[test]
343+
fn list_policies_first_page() {
344+
let (env, contract_id, _, token) = setup();
345+
let client = NiffyInsureClient::new(&env, &contract_id);
346+
let holder = Address::generate(&env);
347+
348+
env.as_contract(&contract_id, || {
349+
for id in 1u32..=5 {
350+
storage::set_policy(&env, &holder, id, &make_policy(&holder, id, &token));
351+
env.storage().persistent().set(
352+
&storage::DataKey::PolicyCounter(holder.clone()),
353+
&id,
354+
);
355+
}
356+
});
357+
358+
let page = client.list_policies(&holder, &0u32, &3u32);
359+
assert_eq!(page.len(), 3u32);
360+
assert_eq!(page.get(0).unwrap().policy_id, 1u32);
361+
assert_eq!(page.get(2).unwrap().policy_id, 3u32);
362+
}
363+
364+
#[test]
365+
fn list_policies_second_page_cursor() {
366+
let (env, contract_id, _, token) = setup();
367+
let client = NiffyInsureClient::new(&env, &contract_id);
368+
let holder = Address::generate(&env);
369+
370+
env.as_contract(&contract_id, || {
371+
for id in 1u32..=5 {
372+
storage::set_policy(&env, &holder, id, &make_policy(&holder, id, &token));
373+
env.storage().persistent().set(
374+
&storage::DataKey::PolicyCounter(holder.clone()),
375+
&id,
376+
);
377+
}
378+
});
379+
380+
let page = client.list_policies(&holder, &3u32, &10u32);
381+
assert_eq!(page.len(), 2u32);
382+
assert_eq!(page.get(0).unwrap().policy_id, 4u32);
383+
assert_eq!(page.get(1).unwrap().policy_id, 5u32);
384+
}
385+
386+
#[test]
387+
fn list_policies_cursor_past_end_returns_empty() {
388+
let (env, contract_id, _, token) = setup();
389+
let client = NiffyInsureClient::new(&env, &contract_id);
390+
let holder = Address::generate(&env);
391+
392+
env.as_contract(&contract_id, || {
393+
storage::set_policy(&env, &holder, 1, &make_policy(&holder, 1, &token));
394+
env.storage().persistent().set(
395+
&storage::DataKey::PolicyCounter(holder.clone()),
396+
&1u32,
397+
);
398+
});
399+
400+
let page = client.list_policies(&holder, &99u32, &10u32);
401+
assert_eq!(page.len(), 0u32);
402+
}
403+
404+
#[test]
405+
fn list_policies_limit_clamped_to_page_size_max() {
406+
let (env, contract_id, _, token) = setup();
407+
let client = NiffyInsureClient::new(&env, &contract_id);
408+
let holder = Address::generate(&env);
409+
410+
env.as_contract(&contract_id, || {
411+
for id in 1u32..=25 {
412+
storage::set_policy(&env, &holder, id, &make_policy(&holder, id, &token));
413+
env.storage().persistent().set(
414+
&storage::DataKey::PolicyCounter(holder.clone()),
415+
&id,
416+
);
417+
}
418+
});
419+
420+
let page = client.list_policies(&holder, &0u32, &100u32);
421+
assert_eq!(page.len(), 20u32);
422+
}
423+
424+
// ── pagination: list_claims ───────────────────────────────────────────────────
425+
426+
fn make_claim(env: &Env, claim_id: u64, holder: &Address) -> niffyinsure::types::Claim {
427+
use niffyinsure::types::{Claim, ClaimStatus};
428+
Claim {
429+
claim_id,
430+
policy_id: 1,
431+
claimant: holder.clone(),
432+
amount: 10_000_000,
433+
details: String::from_str(env, "test"),
434+
image_urls: vec![env],
435+
status: ClaimStatus::Processing,
436+
voting_deadline_ledger: 1000,
437+
approve_votes: 0,
438+
reject_votes: 0,
439+
filed_at: 1,
440+
appeal_open_deadline_ledger: 0,
441+
appeals_count: 0,
442+
appeal_deadline_ledger: 0,
443+
appeal_approve_votes: 0,
444+
appeal_reject_votes: 0,
445+
}
446+
}
447+
448+
#[test]
449+
fn list_claims_empty_when_none_filed() {
450+
let (env, contract_id, _, _) = setup();
451+
let client = NiffyInsureClient::new(&env, &contract_id);
452+
let page = client.list_claims(&0u64, &10u32);
453+
assert_eq!(page.len(), 0u32);
454+
}
455+
456+
#[test]
457+
fn list_claims_first_page() {
458+
let (env, contract_id, _, _) = setup();
459+
let client = NiffyInsureClient::new(&env, &contract_id);
460+
let holder = Address::generate(&env);
461+
462+
env.as_contract(&contract_id, || {
463+
for id in 1u64..=5 {
464+
storage::set_claim(&env, &make_claim(&env, id, &holder));
465+
env.storage().instance().set(&storage::DataKey::ClaimCounter, &id);
466+
}
467+
});
468+
469+
let page = client.list_claims(&0u64, &3u32);
470+
assert_eq!(page.len(), 3u32);
471+
assert_eq!(page.get(0).unwrap().claim_id, 1u64);
472+
assert_eq!(page.get(2).unwrap().claim_id, 3u64);
473+
}
474+
475+
#[test]
476+
fn list_claims_last_page_partial() {
477+
let (env, contract_id, _, _) = setup();
478+
let client = NiffyInsureClient::new(&env, &contract_id);
479+
let holder = Address::generate(&env);
480+
481+
env.as_contract(&contract_id, || {
482+
for id in 1u64..=5 {
483+
storage::set_claim(&env, &make_claim(&env, id, &holder));
484+
env.storage().instance().set(&storage::DataKey::ClaimCounter, &id);
485+
}
486+
});
487+
488+
let page = client.list_claims(&4u64, &10u32);
489+
assert_eq!(page.len(), 1u32);
490+
assert_eq!(page.get(0).unwrap().claim_id, 5u64);
491+
}
492+
493+
#[test]
494+
fn list_claims_cursor_past_end_returns_empty() {
495+
let (env, contract_id, _, _) = setup();
496+
let client = NiffyInsureClient::new(&env, &contract_id);
497+
let holder = Address::generate(&env);
498+
499+
env.as_contract(&contract_id, || {
500+
storage::set_claim(&env, &make_claim(&env, 1, &holder));
501+
env.storage().instance().set(&storage::DataKey::ClaimCounter, &1u64);
502+
});
503+
504+
let page = client.list_claims(&999u64, &10u32);
505+
assert_eq!(page.len(), 0u32);
506+
}
507+
508+
#[test]
509+
fn list_claims_oversize_request_clamped() {
510+
let (env, contract_id, _, _) = setup();
511+
let client = NiffyInsureClient::new(&env, &contract_id);
512+
let holder = Address::generate(&env);
513+
514+
env.as_contract(&contract_id, || {
515+
for id in 1u64..=25 {
516+
storage::set_claim(&env, &make_claim(&env, id, &holder));
517+
env.storage().instance().set(&storage::DataKey::ClaimCounter, &id);
518+
}
519+
});
520+
521+
let page = client.list_claims(&0u64, &999u32);
522+
assert_eq!(page.len(), 20u32);
523+
}
524+
331525
// ── counter immutability: generate_premium does not mutate storage ────────────
332526

333527
#[test]

0 commit comments

Comments
 (0)