11use crate :: {
22 ledger, premium, storage, token,
3- types:: { AgeBand , CoverageType , Policy , PolicyType , PremiumQuote , RegionTier , RiskInput } ,
3+ types:: {
4+ AgeBand , CoverageType , Policy , PolicyType , PremiumQuote , RegionTier , RiskInput ,
5+ STRIKE_DEACTIVATION_THRESHOLD ,
6+ } ,
47 validate:: { self , Error } ,
58} ;
69use soroban_sdk:: { contracterror, contractevent, contracttype, Address , Env , String } ;
@@ -40,6 +43,18 @@ pub enum PolicyError {
4043 NotFound = 111 ,
4144 /// Policy is already active.
4245 AlreadyActive = 112 ,
46+ /// Keeper `process_expired`: policy is not yet at `end_ledger`.
47+ NotYetExpired = 113 ,
48+ /// `renew_policy` called before the renewal window opens.
49+ NotInRenewalWindow = 114 ,
50+ /// `renew_policy`: policy is inactive (terminated or deactivated).
51+ PolicyInactive = 115 ,
52+ /// `renew_policy`: an open claim exists for this policy.
53+ OpenClaimBlocksRenewal = 116 ,
54+ /// `renew_policy`: strike count blocks renewal (see `STRIKE_DEACTIVATION_THRESHOLD`).
55+ TooManyStrikesForRenewal = 117 ,
56+ /// Reserved / legacy: expired renewals now return [`crate::types::RenewPolicyOutcome::Lapsed`] `Ok`.
57+ Expired = 118 ,
4358}
4459
4560#[ contracttype]
@@ -69,7 +84,6 @@ pub struct PolicyInitiated {
6984/// Event emitted by `renew_policy`.
7085#[ contractevent]
7186#[ derive( Clone , Debug ) ]
72- #[ allow( dead_code) ]
7387pub struct PolicyRenewed {
7488 #[ topic]
7589 pub holder : Address ,
@@ -79,6 +93,26 @@ pub struct PolicyRenewed {
7993 pub new_end_ledger : u32 ,
8094}
8195
96+ /// Emitted at most once per `(holder, policy_id, end_ledger)` term when expiry is detected.
97+ ///
98+ /// **Timing:** `reported_at_ledger` is the ledger of the transaction that observes expiry.
99+ /// It may be **strictly greater** than `expiry_ledger` if no call ran exactly at expiry
100+ /// (keeper delay is normal). Indexers and notification services should **deduplicate on
101+ /// `policy_id`** (and holder) and must not assume the event fires on the expiry ledger itself.
102+ ///
103+ /// **`renew_policy` on an expired policy:** the call returns [`crate::types::RenewPolicyOutcome::Lapsed`]
104+ /// in **`Ok`** (not `Err`) so this event and idempotency storage are not rolled back.
105+ #[ contractevent( topics = [ "niffyinsure" , "policy_expired" ] ) ]
106+ #[ derive( Clone , Debug , Eq , PartialEq ) ]
107+ pub struct PolicyExpired {
108+ #[ topic]
109+ pub holder : Address ,
110+ #[ topic]
111+ pub policy_id : u32 ,
112+ pub expiry_ledger : u32 ,
113+ pub reported_at_ledger : u32 ,
114+ }
115+
82116pub fn generate_premium (
83117 env : & Env ,
84118 region : RegionTier ,
@@ -306,3 +340,143 @@ pub fn initiate_policy(
306340
307341 Ok ( policy)
308342}
343+
344+ /// Emit [`PolicyExpired`] if `now >= end_ledger` and we have not yet recorded an event for this term.
345+ pub fn publish_policy_expired_if_due ( env : & Env , policy : & Policy , now : u32 ) {
346+ if !ledger:: is_expired ( now, policy. end_ledger ) {
347+ return ;
348+ }
349+ if storage:: get_policy_expired_event_end_ledger ( env, & policy. holder , policy. policy_id )
350+ == Some ( policy. end_ledger )
351+ {
352+ return ;
353+ }
354+ PolicyExpired {
355+ holder : policy. holder . clone ( ) ,
356+ policy_id : policy. policy_id ,
357+ expiry_ledger : policy. end_ledger ,
358+ reported_at_ledger : now,
359+ }
360+ . publish ( env) ;
361+ storage:: set_policy_expired_event_end_ledger (
362+ env,
363+ & policy. holder ,
364+ policy. policy_id ,
365+ policy. end_ledger ,
366+ ) ;
367+ storage:: bump_instance ( env) ;
368+ }
369+
370+ /// Keeper entrypoint: observe expiry for indexers / notification pipelines.
371+ ///
372+ /// Reverts with [`PolicyError::NotYetExpired`] if `now < end_ledger`. If expiry was already
373+ /// notified for this policy term, succeeds without emitting a duplicate event.
374+ pub fn process_expired ( env : & Env , holder : Address , policy_id : u32 ) -> Result < ( ) , PolicyError > {
375+ storage:: bump_instance ( env) ;
376+ let policy = storage:: get_policy ( env, & holder, policy_id) . ok_or ( PolicyError :: NotFound ) ?;
377+ let now = env. ledger ( ) . sequence ( ) ;
378+ if !ledger:: is_expired ( now, policy. end_ledger ) {
379+ return Err ( PolicyError :: NotYetExpired ) ;
380+ }
381+ publish_policy_expired_if_due ( env, & policy, now) ;
382+ Ok ( ( ) )
383+ }
384+
385+ /// Extend policy duration after premium payment (renewal window only).
386+ ///
387+ /// If the policy is already expired, records [`PolicyExpired`] if not yet recorded for this
388+ /// term, then returns [`crate::types::RenewPolicyOutcome::Lapsed`] in **`Ok`** so storage and
389+ /// events persist (an `Err` would roll back the contract invocation).
390+ #[ allow( clippy:: too_many_arguments) ]
391+ pub fn renew_policy (
392+ env : & Env ,
393+ holder : Address ,
394+ policy_id : u32 ,
395+ age_band : AgeBand ,
396+ coverage_type : CoverageType ,
397+ safety_score : u32 ,
398+ ) -> Result < crate :: types:: RenewPolicyOutcome , PolicyError > {
399+ storage:: assert_bind_not_paused ( env) ;
400+ holder. require_auth ( ) ;
401+
402+ let mut policy = storage:: get_policy ( env, & holder, policy_id) . ok_or ( PolicyError :: NotFound ) ?;
403+ let now = env. ledger ( ) . sequence ( ) ;
404+
405+ if ledger:: is_expired ( now, policy. end_ledger ) {
406+ publish_policy_expired_if_due ( env, & policy, now) ;
407+ return Ok ( crate :: types:: RenewPolicyOutcome :: Lapsed ) ;
408+ }
409+
410+ if !policy. is_active {
411+ return Err ( PolicyError :: PolicyInactive ) ;
412+ }
413+
414+ if storage:: has_open_claim ( env, & holder, policy_id) {
415+ return Err ( PolicyError :: OpenClaimBlocksRenewal ) ;
416+ }
417+
418+ if policy. strike_count >= STRIKE_DEACTIVATION_THRESHOLD {
419+ return Err ( PolicyError :: TooManyStrikesForRenewal ) ;
420+ }
421+
422+ if !ledger:: is_in_renewal_window (
423+ now,
424+ policy. end_ledger ,
425+ ledger:: RENEWAL_WINDOW_LEDGERS ,
426+ ) {
427+ return Err ( PolicyError :: NotInRenewalWindow ) ;
428+ }
429+
430+ if safety_score > 100 {
431+ return Err ( PolicyError :: InvalidRiskScore ) ;
432+ }
433+
434+ if !storage:: is_allowed_asset ( env, & policy. asset ) {
435+ return Err ( PolicyError :: AssetNotAllowed ) ;
436+ }
437+
438+ let input = RiskInput {
439+ region : policy. region . clone ( ) ,
440+ age_band : age_band. clone ( ) ,
441+ coverage : coverage_type,
442+ safety_score,
443+ } ;
444+
445+ let quote =
446+ crate :: calculator:: compute_quote ( env, & input, policy. coverage , false , QUOTE_TTL_LEDGERS )
447+ . map_err ( |e| match e {
448+ Error :: CalculatorPaused => PolicyError :: ContractPaused ,
449+ Error :: CalculatorCallFailed | Error :: CalculatorNotSet => PolicyError :: PremiumOverflow ,
450+ _ => PolicyError :: PremiumOverflow ,
451+ } ) ?;
452+
453+ let premium_amount = quote. total_premium ;
454+ if premium_amount <= 0 {
455+ return Err ( PolicyError :: InvalidPremium ) ;
456+ }
457+
458+ token:: collect_premium ( env, & holder, & policy. asset , premium_amount) ;
459+
460+ let new_end = policy
461+ . end_ledger
462+ . checked_add ( ledger:: POLICY_DURATION_LEDGERS )
463+ . ok_or ( PolicyError :: LedgerOverflow ) ?;
464+
465+ policy. premium = premium_amount;
466+ policy. end_ledger = new_end;
467+
468+ validate:: check_policy ( & policy) . map_err ( |_| PolicyError :: PolicyValidation ) ?;
469+
470+ storage:: set_policy ( env, & holder, policy_id, & policy) ;
471+
472+ PolicyRenewed {
473+ version : POLICY_EVENT_VERSION ,
474+ holder : holder. clone ( ) ,
475+ policy_id,
476+ premium : premium_amount,
477+ new_end_ledger : new_end,
478+ }
479+ . publish ( env) ;
480+
481+ Ok ( crate :: types:: RenewPolicyOutcome :: Renewed ( policy) )
482+ }
0 commit comments