@@ -39,6 +39,18 @@ pub enum PolicyError {
3939 NotFound = 111 ,
4040 /// Policy is already active.
4141 AlreadyActive = 112 ,
42+ /// Keeper `process_expired`: policy is not yet at `end_ledger`.
43+ NotYetExpired = 113 ,
44+ /// `renew_policy` called before the renewal window opens.
45+ NotInRenewalWindow = 114 ,
46+ /// `renew_policy`: policy is inactive (terminated or deactivated).
47+ PolicyInactive = 115 ,
48+ /// `renew_policy`: an open claim exists for this policy.
49+ OpenClaimBlocksRenewal = 116 ,
50+ /// `renew_policy`: strike count blocks renewal (see `STRIKE_DEACTIVATION_THRESHOLD`).
51+ TooManyStrikesForRenewal = 117 ,
52+ /// Reserved / legacy: expired renewals now return [`crate::types::RenewPolicyOutcome::Lapsed`] `Ok`.
53+ Expired = 118 ,
4254}
4355
4456#[ contracttype]
@@ -80,7 +92,6 @@ pub struct BeneficiaryUpdated {
8092/// Event emitted by `renew_policy`.
8193#[ contractevent]
8294#[ derive( Clone , Debug ) ]
83- #[ allow( dead_code) ]
8495pub struct PolicyRenewed {
8596 #[ topic]
8697 pub holder : Address ,
@@ -90,6 +101,26 @@ pub struct PolicyRenewed {
90101 pub new_end_ledger : u32 ,
91102}
92103
104+ /// Emitted at most once per `(holder, policy_id, end_ledger)` term when expiry is detected.
105+ ///
106+ /// **Timing:** `reported_at_ledger` is the ledger of the transaction that observes expiry.
107+ /// It may be **strictly greater** than `expiry_ledger` if no call ran exactly at expiry
108+ /// (keeper delay is normal). Indexers and notification services should **deduplicate on
109+ /// `policy_id`** (and holder) and must not assume the event fires on the expiry ledger itself.
110+ ///
111+ /// **`renew_policy` on an expired policy:** the call returns [`crate::types::RenewPolicyOutcome::Lapsed`]
112+ /// in **`Ok`** (not `Err`) so this event and idempotency storage are not rolled back.
113+ #[ contractevent( topics = [ "niffyinsure" , "policy_expired" ] ) ]
114+ #[ derive( Clone , Debug , Eq , PartialEq ) ]
115+ pub struct PolicyExpired {
116+ #[ topic]
117+ pub holder : Address ,
118+ #[ topic]
119+ pub policy_id : u32 ,
120+ pub expiry_ledger : u32 ,
121+ pub reported_at_ledger : u32 ,
122+ }
123+
93124pub fn generate_premium (
94125 env : & Env ,
95126 region : RegionTier ,
@@ -507,3 +538,143 @@ pub fn renew_policy(
507538
508539 Ok ( policy)
509540}
541+
542+ /// Emit [`PolicyExpired`] if `now >= end_ledger` and we have not yet recorded an event for this term.
543+ pub fn publish_policy_expired_if_due ( env : & Env , policy : & Policy , now : u32 ) {
544+ if !ledger:: is_expired ( now, policy. end_ledger ) {
545+ return ;
546+ }
547+ if storage:: get_policy_expired_event_end_ledger ( env, & policy. holder , policy. policy_id )
548+ == Some ( policy. end_ledger )
549+ {
550+ return ;
551+ }
552+ PolicyExpired {
553+ holder : policy. holder . clone ( ) ,
554+ policy_id : policy. policy_id ,
555+ expiry_ledger : policy. end_ledger ,
556+ reported_at_ledger : now,
557+ }
558+ . publish ( env) ;
559+ storage:: set_policy_expired_event_end_ledger (
560+ env,
561+ & policy. holder ,
562+ policy. policy_id ,
563+ policy. end_ledger ,
564+ ) ;
565+ storage:: bump_instance ( env) ;
566+ }
567+
568+ /// Keeper entrypoint: observe expiry for indexers / notification pipelines.
569+ ///
570+ /// Reverts with [`PolicyError::NotYetExpired`] if `now < end_ledger`. If expiry was already
571+ /// notified for this policy term, succeeds without emitting a duplicate event.
572+ pub fn process_expired ( env : & Env , holder : Address , policy_id : u32 ) -> Result < ( ) , PolicyError > {
573+ storage:: bump_instance ( env) ;
574+ let policy = storage:: get_policy ( env, & holder, policy_id) . ok_or ( PolicyError :: NotFound ) ?;
575+ let now = env. ledger ( ) . sequence ( ) ;
576+ if !ledger:: is_expired ( now, policy. end_ledger ) {
577+ return Err ( PolicyError :: NotYetExpired ) ;
578+ }
579+ publish_policy_expired_if_due ( env, & policy, now) ;
580+ Ok ( ( ) )
581+ }
582+
583+ /// Extend policy duration after premium payment (renewal window only).
584+ ///
585+ /// If the policy is already expired, records [`PolicyExpired`] if not yet recorded for this
586+ /// term, then returns [`crate::types::RenewPolicyOutcome::Lapsed`] in **`Ok`** so storage and
587+ /// events persist (an `Err` would roll back the contract invocation).
588+ #[ allow( clippy:: too_many_arguments) ]
589+ pub fn renew_policy (
590+ env : & Env ,
591+ holder : Address ,
592+ policy_id : u32 ,
593+ age_band : AgeBand ,
594+ coverage_type : CoverageType ,
595+ safety_score : u32 ,
596+ ) -> Result < crate :: types:: RenewPolicyOutcome , PolicyError > {
597+ storage:: assert_bind_not_paused ( env) ;
598+ holder. require_auth ( ) ;
599+
600+ let mut policy = storage:: get_policy ( env, & holder, policy_id) . ok_or ( PolicyError :: NotFound ) ?;
601+ let now = env. ledger ( ) . sequence ( ) ;
602+
603+ if ledger:: is_expired ( now, policy. end_ledger ) {
604+ publish_policy_expired_if_due ( env, & policy, now) ;
605+ return Ok ( crate :: types:: RenewPolicyOutcome :: Lapsed ) ;
606+ }
607+
608+ if !policy. is_active {
609+ return Err ( PolicyError :: PolicyInactive ) ;
610+ }
611+
612+ if storage:: has_open_claim ( env, & holder, policy_id) {
613+ return Err ( PolicyError :: OpenClaimBlocksRenewal ) ;
614+ }
615+
616+ if policy. strike_count >= STRIKE_DEACTIVATION_THRESHOLD {
617+ return Err ( PolicyError :: TooManyStrikesForRenewal ) ;
618+ }
619+
620+ if !ledger:: is_in_renewal_window (
621+ now,
622+ policy. end_ledger ,
623+ ledger:: RENEWAL_WINDOW_LEDGERS ,
624+ ) {
625+ return Err ( PolicyError :: NotInRenewalWindow ) ;
626+ }
627+
628+ if safety_score > 100 {
629+ return Err ( PolicyError :: InvalidRiskScore ) ;
630+ }
631+
632+ if !storage:: is_allowed_asset ( env, & policy. asset ) {
633+ return Err ( PolicyError :: AssetNotAllowed ) ;
634+ }
635+
636+ let input = RiskInput {
637+ region : policy. region . clone ( ) ,
638+ age_band : age_band. clone ( ) ,
639+ coverage : coverage_type,
640+ safety_score,
641+ } ;
642+
643+ let quote =
644+ crate :: calculator:: compute_quote ( env, & input, policy. coverage , false , QUOTE_TTL_LEDGERS )
645+ . map_err ( |e| match e {
646+ Error :: CalculatorPaused => PolicyError :: ContractPaused ,
647+ Error :: CalculatorCallFailed | Error :: CalculatorNotSet => PolicyError :: PremiumOverflow ,
648+ _ => PolicyError :: PremiumOverflow ,
649+ } ) ?;
650+
651+ let premium_amount = quote. total_premium ;
652+ if premium_amount <= 0 {
653+ return Err ( PolicyError :: InvalidPremium ) ;
654+ }
655+
656+ token:: collect_premium ( env, & holder, & policy. asset , premium_amount) ;
657+
658+ let new_end = policy
659+ . end_ledger
660+ . checked_add ( ledger:: POLICY_DURATION_LEDGERS )
661+ . ok_or ( PolicyError :: LedgerOverflow ) ?;
662+
663+ policy. premium = premium_amount;
664+ policy. end_ledger = new_end;
665+
666+ validate:: check_policy ( & policy) . map_err ( |_| PolicyError :: PolicyValidation ) ?;
667+
668+ storage:: set_policy ( env, & holder, policy_id, & policy) ;
669+
670+ PolicyRenewed {
671+ version : POLICY_EVENT_VERSION ,
672+ holder : holder. clone ( ) ,
673+ policy_id,
674+ premium : premium_amount,
675+ new_end_ledger : new_end,
676+ }
677+ . publish ( env) ;
678+
679+ Ok ( crate :: types:: RenewPolicyOutcome :: Renewed ( policy) )
680+ }
0 commit comments