Skip to content

Commit 8f55b84

Browse files
authored
Merge pull request #171 from Macnelson9/getters
feat: Read/query entrypoints: get_policy, get_claim, bounded pagination
2 parents f9ad692 + 0a5c2b2 commit 8f55b84

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
@@ -234,6 +234,41 @@ impl NiffyInsure {
234234
storage::get_claim_counter(&env)
235235
}
236236

237+
/// Paginated listing of claims by claim_id range, ordered ascending.
238+
///
239+
/// `start_after` is an exclusive cursor: pass `0` for the first page, or the
240+
/// last `claim_id` received to advance to the next page.
241+
/// `limit` is capped at `PAGE_SIZE_MAX` (20); larger values are silently clamped.
242+
///
243+
/// Returns summary structs — call `get_claim` for the full record.
244+
///
245+
/// Empty page (len == 0) means no more results exist beyond the cursor.
246+
/// Because claim_ids are monotonically increasing and never deleted, a
247+
/// stale cursor never panics — it simply returns an empty page.
248+
pub fn list_claims(
249+
env: Env,
250+
start_after: u64,
251+
limit: u32,
252+
) -> Vec<types::ClaimSummary> {
253+
let cap = limit.min(types::PAGE_SIZE_MAX);
254+
let total = storage::get_claim_counter(&env);
255+
let mut results: Vec<types::ClaimSummary> = Vec::new(&env);
256+
let mut id: u64 = start_after.saturating_add(1);
257+
while id <= total && results.len() < cap {
258+
if let Some(c) = storage::get_claim(&env, id) {
259+
results.push_back(types::ClaimSummary {
260+
claim_id: c.claim_id,
261+
policy_id: c.policy_id,
262+
amount: c.amount,
263+
status: c.status,
264+
filed_at: c.filed_at,
265+
});
266+
}
267+
id = id.saturating_add(1);
268+
}
269+
results
270+
}
271+
237272
pub fn get_policy_counter(env: Env, holder: Address) -> u32 {
238273
storage::get_policy_counter(&env, &holder)
239274
}
@@ -311,6 +346,42 @@ impl NiffyInsure {
311346
storage::get_policy(&env, &holder, policy_id)
312347
}
313348

349+
/// Paginated listing of a holder's policies, ordered by ascending policy_id.
350+
///
351+
/// `start_after` is an exclusive cursor: pass `0` for the first page, or the
352+
/// last `policy_id` received to advance to the next page.
353+
/// `limit` is capped at `PAGE_SIZE_MAX` (20); larger values are silently clamped.
354+
///
355+
/// Returns summary structs — call `get_policy` for the full record.
356+
///
357+
/// Empty page (len == 0) means no more results exist beyond the cursor.
358+
/// Because policy_ids are monotonically increasing and never deleted, a
359+
/// stale cursor never panics — it simply returns an empty page.
360+
pub fn list_policies(
361+
env: Env,
362+
holder: Address,
363+
start_after: u32,
364+
limit: u32,
365+
) -> Vec<types::PolicySummary> {
366+
let cap = limit.min(types::PAGE_SIZE_MAX);
367+
let total = storage::get_policy_counter(&env, &holder);
368+
let mut results: Vec<types::PolicySummary> = Vec::new(&env);
369+
let mut id: u32 = start_after.saturating_add(1);
370+
while id <= total && results.len() < cap {
371+
if let Some(p) = storage::get_policy(&env, &holder, id) {
372+
results.push_back(types::PolicySummary {
373+
policy_id: p.policy_id,
374+
policy_type: p.policy_type,
375+
coverage: p.coverage,
376+
is_active: p.is_active,
377+
end_ledger: p.end_ledger,
378+
});
379+
}
380+
id = id.saturating_add(1);
381+
}
382+
results
383+
}
384+
314385
/// Read-only: number of active policies for a holder (= vote weight).
315386
pub fn get_active_policy_count(env: Env, holder: Address) -> u32 {
316387
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)