@@ -21,6 +21,8 @@ pub enum PolicyError {
2121 LedgerOverflow = 8 ,
2222 InvalidTerminationReason = 9 ,
2323 HolderMismatch = 10 ,
24+ /// Permissionless `process_expired`: ledger is before `end_ledger + grace_period_ledgers`.
25+ PolicyLapseNotReached = 11 ,
2426}
2527
2628#[ allow( dead_code) ]
@@ -115,6 +117,68 @@ pub fn admin_terminate_policy(
115117 terminate_inner ( env, & holder, policy_id, reason, true , allow_open_claims)
116118}
117119
120+ /// Permissionless keeper: mark a policy inactive after the renewal + grace window has ended.
121+ ///
122+ /// Policies are keyed by `(holder, policy_id)`; `holder` is a **lookup key only** (no auth).
123+ /// Eligible when `now >= end_ledger + grace_period_ledgers`, the policy is still active, and
124+ /// there is no open claim on that policy. Idempotent: if already inactive, returns `Ok(())`
125+ /// and emits nothing.
126+ ///
127+ /// Uses [`TerminationReason::LapsedNonPayment`] and emits [`PolicyExpired`] (distinct from
128+ /// holder/admin [`PolicyTerminated`]).
129+ pub fn process_expired ( env : & Env , holder : Address , policy_id : u32 ) -> Result < ( ) , PolicyError > {
130+ let mut policy = storage:: get_policy ( env, & holder, policy_id) . ok_or ( PolicyError :: PolicyNotFound ) ?;
131+
132+ if !policy. is_active {
133+ return Ok ( ( ) ) ;
134+ }
135+
136+ let now = env. ledger ( ) . sequence ( ) ;
137+ let grace = storage:: get_grace_period_ledgers ( env) ;
138+ let lapse_ledger = policy
139+ . end_ledger
140+ . checked_add ( grace)
141+ . ok_or ( PolicyError :: LedgerOverflow ) ?;
142+
143+ if now < lapse_ledger {
144+ return Err ( PolicyError :: PolicyLapseNotReached ) ;
145+ }
146+
147+ if storage:: has_open_claim ( env, & holder, policy_id) {
148+ return Err ( PolicyError :: OpenClaimsMustFinalize ) ;
149+ }
150+
151+ policy. is_active = false ;
152+ policy. terminated_at_ledger = now;
153+ policy. termination_reason = TerminationReason :: LapsedNonPayment ;
154+ policy. terminated_by_admin = false ;
155+
156+ storage:: set_policy ( env, & holder, policy_id, & policy) ;
157+ storage:: decrement_holder_active_policies ( env, & holder) ;
158+ if storage:: get_holder_active_policy_count ( env, & holder) == 0 {
159+ storage:: voters_remove_holder ( env, & holder) ;
160+ }
161+
162+ PolicyExpired {
163+ holder : holder. clone ( ) ,
164+ policy_id,
165+ at_ledger : now,
166+ }
167+ . publish ( env) ;
168+
169+ Ok ( ( ) )
170+ }
171+
172+ #[ contractevent( topics = [ "niffyinsure" , "policy_expired" ] ) ]
173+ #[ derive( Clone , Debug , Eq , PartialEq ) ]
174+ pub struct PolicyExpired {
175+ #[ topic]
176+ pub holder : Address ,
177+ #[ topic]
178+ pub policy_id : u32 ,
179+ pub at_ledger : u32 ,
180+ }
181+
118182fn terminate_inner (
119183 env : & Env ,
120184 holder : & Address ,
0 commit comments