@@ -4,7 +4,6 @@ use crate::{
44 validate:: { self , Error } ,
55} ;
66use soroban_sdk:: { contracterror, contractevent, contracttype, Address , Env , String } ;
7-
87pub use ledger:: QUOTE_TTL_LEDGERS ;
98
109/// Current event schema version.
@@ -368,3 +367,143 @@ pub fn set_beneficiary(
368367
369368 Ok ( ( ) )
370369}
370+
371+ // ── Grace period admin setter ─────────────────────────────────────────────────
372+
373+ #[ contractevent( topics = [ "niffyinsure" , "grace_period_updated" ] ) ]
374+ #[ derive( Clone , Debug , Eq , PartialEq ) ]
375+ pub struct GracePeriodUpdated {
376+ pub old_ledgers : u32 ,
377+ pub new_ledgers : u32 ,
378+ }
379+
380+ pub fn set_grace_period_ledgers ( env : & Env , ledgers : u32 ) -> Result < ( ) , RenewalError > {
381+ crate :: admin:: require_admin ( env) ;
382+ if !ledger:: is_valid_grace_period_ledgers ( ledgers) {
383+ return Err ( RenewalError :: GracePeriodOutOfBounds ) ;
384+ }
385+ let old = storage:: get_grace_period_ledgers ( env) ;
386+ storage:: set_grace_period_ledgers ( env, ledgers) ;
387+ GracePeriodUpdated {
388+ old_ledgers : old,
389+ new_ledgers : ledgers,
390+ }
391+ . publish ( env) ;
392+ Ok ( ( ) )
393+ }
394+
395+ pub fn get_grace_period_ledgers ( env : & Env ) -> u32 {
396+ storage:: get_grace_period_ledgers ( env)
397+ }
398+
399+ // ── renew_policy ──────────────────────────────────────────────────────────────
400+
401+ #[ contracterror]
402+ #[ derive( Copy , Clone , Debug , Eq , PartialEq , PartialOrd , Ord ) ]
403+ #[ repr( u32 ) ]
404+ pub enum RenewalError {
405+ /// Policy not found.
406+ NotFound = 200 ,
407+ /// Policy is not active.
408+ Inactive = 201 ,
409+ /// Current ledger is outside the renewal + grace window.
410+ WindowClosed = 202 ,
411+ /// An open claim is blocking renewal.
412+ OpenClaimBlocking = 203 ,
413+ /// Premium computation failed.
414+ PremiumError = 204 ,
415+ /// Ledger arithmetic overflow.
416+ LedgerOverflow = 205 ,
417+ /// Grace period value outside allowed [min, max] range.
418+ GracePeriodOutOfBounds = 206 ,
419+ }
420+
421+ /// Renew an existing active policy.
422+ ///
423+ /// Eligible window: `[end - RENEWAL_WINDOW_LEDGERS, end + grace_period_ledgers)`.
424+ /// Renewal within the grace period succeeds with no coverage gap — the new
425+ /// term starts at `old_end_ledger + 1`.
426+ /// Blocked when an open claim exists on the policy (mirrors the open-claim rule).
427+ pub fn renew_policy (
428+ env : & Env ,
429+ holder : Address ,
430+ policy_id : u32 ,
431+ age_band : crate :: types:: AgeBand ,
432+ coverage_type : CoverageTier ,
433+ safety_score : u32 ,
434+ base_amount : i128 ,
435+ ) -> Result < Policy , RenewalError > {
436+ storage:: assert_bind_not_paused ( env) ;
437+ holder. require_auth ( ) ;
438+
439+ let mut policy = storage:: get_policy ( env, & holder, policy_id)
440+ . ok_or ( RenewalError :: NotFound ) ?;
441+
442+ if !policy. is_active {
443+ return Err ( RenewalError :: Inactive ) ;
444+ }
445+
446+ // Open-claim guard (mirrors the rule enforced in the backend).
447+ if storage:: has_open_claim ( env, & holder, policy_id) {
448+ return Err ( RenewalError :: OpenClaimBlocking ) ;
449+ }
450+
451+ let now = env. ledger ( ) . sequence ( ) ;
452+ let grace = storage:: get_grace_period_ledgers ( env) ;
453+
454+ if !ledger:: is_in_renewal_window_with_grace (
455+ now,
456+ policy. end_ledger ,
457+ ledger:: RENEWAL_WINDOW_LEDGERS ,
458+ grace,
459+ ) {
460+ return Err ( RenewalError :: WindowClosed ) ;
461+ }
462+
463+ // Recalculate premium with the same deterministic formula.
464+ let input = RiskInput {
465+ region : policy. region . clone ( ) ,
466+ age_band,
467+ coverage : coverage_type,
468+ safety_score,
469+ } ;
470+ let quote = crate :: calculator:: compute_quote (
471+ env,
472+ & input,
473+ base_amount,
474+ false ,
475+ ledger:: QUOTE_TTL_LEDGERS ,
476+ )
477+ . map_err ( |_| RenewalError :: PremiumError ) ?;
478+
479+ let premium_amount = quote. total_premium ;
480+ if premium_amount <= 0 {
481+ return Err ( RenewalError :: PremiumError ) ;
482+ }
483+
484+ // Collect premium before any state mutation.
485+ token:: collect_premium ( env, & holder, & policy. asset , premium_amount) ;
486+
487+ // New term starts immediately after old end — no gap, no overlap.
488+ let new_start = policy. end_ledger . saturating_add ( 1 ) ;
489+ let new_end = new_start
490+ . checked_add ( ledger:: POLICY_DURATION_LEDGERS )
491+ . ok_or ( RenewalError :: LedgerOverflow ) ?;
492+
493+ policy. start_ledger = new_start;
494+ policy. end_ledger = new_end;
495+ policy. premium = premium_amount;
496+
497+ storage:: set_policy ( env, & holder, policy_id, & policy) ;
498+
499+ PolicyRenewed {
500+ version : POLICY_EVENT_VERSION ,
501+ policy_id,
502+ holder : holder. clone ( ) ,
503+ premium : premium_amount,
504+ new_end_ledger : new_end,
505+ }
506+ . publish ( env) ;
507+
508+ Ok ( policy)
509+ }
0 commit comments