@@ -14,7 +14,7 @@ use ink::storage::Mapping;
1414mod propchain_insurance {
1515 use super :: * ;
1616 use ink:: prelude:: { string:: String , vec:: Vec } ;
17- use propchain_contracts :: { non_reentrant, ReentrancyError , ReentrancyGuard } ;
17+ use propchain_traits :: { non_reentrant, ReentrancyError , ReentrancyGuard } ;
1818
1919 // Error types extracted to errors.rs (Issue #101)
2020 include ! ( "errors.rs" ) ;
@@ -92,6 +92,11 @@ mod propchain_insurance {
9292 // Claim cooldown: property_id -> last_claim_timestamp
9393 claim_cooldowns : Mapping < u64 , u64 > ,
9494
95+ // Claim automation: oracle-triggered parametric claims
96+ claim_triggers : Mapping < u64 , ClaimTrigger > ,
97+ trigger_count : u64 ,
98+ policy_triggers : Mapping < u64 , Vec < u64 > > , // policy_id -> trigger_ids
99+
95100 // Platform settings
96101 platform_fee_rate : u32 , // Basis points (e.g. 200 = 2%)
97102 claim_cooldown_period : u64 , // In seconds
@@ -339,6 +344,9 @@ mod propchain_insurance {
339344 authorized_oracles : Mapping :: default ( ) ,
340345 authorized_assessors : Mapping :: default ( ) ,
341346 claim_cooldowns : Mapping :: default ( ) ,
347+ claim_triggers : Mapping :: default ( ) ,
348+ trigger_count : 0 ,
349+ policy_triggers : Mapping :: default ( ) ,
342350 platform_fee_rate : 200 , // 2%
343351 claim_cooldown_period : 2_592_000 , // 30 days in seconds
344352 min_pool_capital : 100_000_000_000 , // Minimum pool capital
@@ -862,6 +870,266 @@ mod propchain_insurance {
862870 } )
863871 }
864872
873+ // =====================================================================
874+ // CLAIM AUTOMATION (oracle-triggered parametric claims)
875+ // =====================================================================
876+
877+ /// Register an oracle-driven claim trigger against a policy. The
878+ /// policyholder or admin may register; once an authorized oracle reports
879+ /// a value satisfying the comparator/threshold via `report_oracle_event`,
880+ /// the contract creates, approves, and pays a claim automatically.
881+ #[ ink( message) ]
882+ pub fn register_claim_trigger (
883+ & mut self ,
884+ policy_id : u64 ,
885+ metric : TriggerMetric ,
886+ comparator : TriggerComparator ,
887+ threshold : u128 ,
888+ payout_mode : PayoutMode ,
889+ ) -> Result < u64 , InsuranceError > {
890+ let caller = self . env ( ) . caller ( ) ;
891+
892+ let policy = self
893+ . policies
894+ . get ( & policy_id)
895+ . ok_or ( InsuranceError :: PolicyNotFound ) ?;
896+ if caller != policy. policyholder && caller != self . admin {
897+ return Err ( InsuranceError :: Unauthorized ) ;
898+ }
899+ if policy. status != PolicyStatus :: Active {
900+ return Err ( InsuranceError :: PolicyInactive ) ;
901+ }
902+ Self :: ensure_payout_mode_valid ( & payout_mode) ?;
903+
904+ let trigger_id = self . trigger_count + 1 ;
905+ self . trigger_count = trigger_id;
906+ let now = self . env ( ) . block_timestamp ( ) ;
907+
908+ let trigger = ClaimTrigger {
909+ trigger_id,
910+ policy_id,
911+ metric,
912+ comparator,
913+ threshold,
914+ payout_mode,
915+ is_active : true ,
916+ triggered : false ,
917+ last_observed_value : None ,
918+ last_report_url : String :: new ( ) ,
919+ created_at : now,
920+ triggered_at : None ,
921+ triggering_claim_id : None ,
922+ } ;
923+ self . claim_triggers . insert ( & trigger_id, & trigger) ;
924+
925+ let mut list = self . policy_triggers . get ( & policy_id) . unwrap_or_default ( ) ;
926+ list. push ( trigger_id) ;
927+ self . policy_triggers . insert ( & policy_id, & list) ;
928+
929+ self . env ( ) . emit_event ( ClaimTriggerRegistered {
930+ trigger_id,
931+ policy_id,
932+ metric,
933+ threshold,
934+ } ) ;
935+
936+ Ok ( trigger_id)
937+ }
938+
939+ /// Deactivate an active trigger. Only the policyholder or admin may
940+ /// deactivate. Already-fired triggers cannot be re-deactivated.
941+ #[ ink( message) ]
942+ pub fn deactivate_claim_trigger (
943+ & mut self ,
944+ trigger_id : u64 ,
945+ ) -> Result < ( ) , InsuranceError > {
946+ let caller = self . env ( ) . caller ( ) ;
947+ let mut trigger = self
948+ . claim_triggers
949+ . get ( & trigger_id)
950+ . ok_or ( InsuranceError :: TriggerNotFound ) ?;
951+ if !trigger. is_active {
952+ return Err ( InsuranceError :: TriggerInactive ) ;
953+ }
954+
955+ let policy = self
956+ . policies
957+ . get ( & trigger. policy_id )
958+ . ok_or ( InsuranceError :: PolicyNotFound ) ?;
959+ if caller != policy. policyholder && caller != self . admin {
960+ return Err ( InsuranceError :: Unauthorized ) ;
961+ }
962+
963+ trigger. is_active = false ;
964+ self . claim_triggers . insert ( & trigger_id, & trigger) ;
965+
966+ self . env ( ) . emit_event ( ClaimTriggerDeactivated {
967+ trigger_id,
968+ policy_id : trigger. policy_id ,
969+ } ) ;
970+ Ok ( ( ) )
971+ }
972+
973+ /// Oracle entry point: report an observed value for a trigger. If the
974+ /// value meets the trigger condition and the underlying policy is
975+ /// still payable, this auto-creates an approved claim and runs the
976+ /// payout in one transaction.
977+ ///
978+ /// Callers must be admin or an authorized oracle. The trigger fires
979+ /// at most once. If the condition is not met, the report is recorded
980+ /// but no claim is created.
981+ #[ ink( message) ]
982+ pub fn report_oracle_event (
983+ & mut self ,
984+ trigger_id : u64 ,
985+ observed_value : u128 ,
986+ oracle_report_url : String ,
987+ ) -> Result < Option < u64 > , InsuranceError > {
988+ non_reentrant ! ( self , {
989+ let caller = self . env( ) . caller( ) ;
990+ if caller != self . admin && !self . authorized_oracles. get( & caller) . unwrap_or( false ) {
991+ return Err ( InsuranceError :: Unauthorized ) ;
992+ }
993+
994+ let mut trigger = self
995+ . claim_triggers
996+ . get( & trigger_id)
997+ . ok_or( InsuranceError :: TriggerNotFound ) ?;
998+ if !trigger. is_active {
999+ return Err ( InsuranceError :: TriggerInactive ) ;
1000+ }
1001+ if trigger. triggered {
1002+ return Err ( InsuranceError :: TriggerAlreadyFired ) ;
1003+ }
1004+
1005+ let now = self . env( ) . block_timestamp( ) ;
1006+ let condition_met = Self :: evaluate_condition(
1007+ & trigger. comparator,
1008+ observed_value,
1009+ trigger. threshold,
1010+ ) ;
1011+
1012+ trigger. last_observed_value = Some ( observed_value) ;
1013+ trigger. last_report_url = oracle_report_url. clone( ) ;
1014+
1015+ self . env( ) . emit_event( OracleEventReceived {
1016+ trigger_id,
1017+ oracle: caller,
1018+ observed_value,
1019+ threshold_met: condition_met,
1020+ timestamp: now,
1021+ } ) ;
1022+
1023+ if !condition_met {
1024+ self . claim_triggers. insert( & trigger_id, & trigger) ;
1025+ return Ok ( None ) ;
1026+ }
1027+
1028+ let mut policy = self
1029+ . policies
1030+ . get( & trigger. policy_id)
1031+ . ok_or( InsuranceError :: PolicyNotFound ) ?;
1032+ if policy. status != PolicyStatus :: Active {
1033+ return Err ( InsuranceError :: PolicyInactive ) ;
1034+ }
1035+ if now > policy. end_time {
1036+ return Err ( InsuranceError :: PolicyExpired ) ;
1037+ }
1038+
1039+ let last_claim = self . claim_cooldowns. get( & policy. property_id) . unwrap_or( 0 ) ;
1040+ if now. saturating_sub( last_claim) < self . claim_cooldown_period {
1041+ return Err ( InsuranceError :: CooldownPeriodActive ) ;
1042+ }
1043+
1044+ let remaining = policy. coverage_amount. saturating_sub( policy. total_claimed) ;
1045+ if remaining == 0 {
1046+ return Err ( InsuranceError :: ClaimExceedsCoverage ) ;
1047+ }
1048+ let claim_amount =
1049+ Self :: compute_claim_amount( & trigger. payout_mode, remaining) ?;
1050+ if claim_amount == 0 {
1051+ return Err ( InsuranceError :: TriggerConditionNotMet ) ;
1052+ }
1053+ let payout = claim_amount. saturating_sub( policy. deductible) ;
1054+
1055+ // Create the auto-claim record.
1056+ let claim_id = self . claim_count + 1 ;
1057+ self . claim_count = claim_id;
1058+
1059+ let mut claim = InsuranceClaim {
1060+ claim_id,
1061+ policy_id: trigger. policy_id,
1062+ claimant: policy. policyholder,
1063+ claim_amount,
1064+ description: String :: from( "Oracle-triggered parametric claim" ) ,
1065+ evidence_url: String :: new( ) ,
1066+ oracle_report_url: oracle_report_url. clone( ) ,
1067+ status: ClaimStatus :: OracleVerifying ,
1068+ submitted_at: now,
1069+ processed_at: Some ( now) ,
1070+ payout_amount: payout,
1071+ assessor: Some ( caller) ,
1072+ rejection_reason: String :: new( ) ,
1073+ } ;
1074+
1075+ policy. claims_count += 1 ;
1076+ self . policies. insert( & trigger. policy_id, & policy) ;
1077+
1078+ let mut policy_claims =
1079+ self . policy_claims. get( & trigger. policy_id) . unwrap_or_default( ) ;
1080+ policy_claims. push( claim_id) ;
1081+ self . policy_claims
1082+ . insert( & trigger. policy_id, & policy_claims) ;
1083+
1084+ claim. status = ClaimStatus :: Approved ;
1085+ self . claims. insert( & claim_id, & claim) ;
1086+
1087+ self . env( ) . emit_event( ClaimApproved {
1088+ claim_id,
1089+ policy_id: trigger. policy_id,
1090+ payout_amount: payout,
1091+ approved_by: caller,
1092+ timestamp: now,
1093+ } ) ;
1094+
1095+ // Run payout (debits pool, marks claim Paid, updates cooldown).
1096+ self . execute_payout( claim_id, trigger. policy_id, policy. policyholder, payout) ?;
1097+
1098+ trigger. triggered = true ;
1099+ trigger. triggered_at = Some ( now) ;
1100+ trigger. triggering_claim_id = Some ( claim_id) ;
1101+ self . claim_triggers. insert( & trigger_id, & trigger) ;
1102+
1103+ self . env( ) . emit_event( ClaimAutoPaid {
1104+ trigger_id,
1105+ claim_id,
1106+ policy_id: trigger. policy_id,
1107+ payout_amount: payout,
1108+ timestamp: now,
1109+ } ) ;
1110+
1111+ Ok ( Some ( claim_id) )
1112+ } )
1113+ }
1114+
1115+ /// Get claim trigger by id.
1116+ #[ ink( message) ]
1117+ pub fn get_claim_trigger ( & self , trigger_id : u64 ) -> Option < ClaimTrigger > {
1118+ self . claim_triggers . get ( & trigger_id)
1119+ }
1120+
1121+ /// Get all trigger ids registered against a policy.
1122+ #[ ink( message) ]
1123+ pub fn get_policy_triggers ( & self , policy_id : u64 ) -> Vec < u64 > {
1124+ self . policy_triggers . get ( & policy_id) . unwrap_or_default ( )
1125+ }
1126+
1127+ /// Total number of triggers ever registered.
1128+ #[ ink( message) ]
1129+ pub fn get_trigger_count ( & self ) -> u64 {
1130+ self . trigger_count
1131+ }
1132+
8651133 // =====================================================================
8661134 // REINSURANCE
8671135 // =====================================================================
@@ -1981,6 +2249,51 @@ mod propchain_insurance {
19812249 Ok ( ( ) )
19822250 }
19832251
2252+ fn evaluate_condition (
2253+ comparator : & TriggerComparator ,
2254+ observed : u128 ,
2255+ threshold : u128 ,
2256+ ) -> bool {
2257+ match comparator {
2258+ TriggerComparator :: GreaterOrEqual => observed >= threshold,
2259+ TriggerComparator :: LessOrEqual => observed <= threshold,
2260+ }
2261+ }
2262+
2263+ fn compute_claim_amount (
2264+ mode : & PayoutMode ,
2265+ remaining_coverage : u128 ,
2266+ ) -> Result < u128 , InsuranceError > {
2267+ let amount = match mode {
2268+ PayoutMode :: Fixed ( v) => ( * v) . min ( remaining_coverage) ,
2269+ PayoutMode :: PercentBps ( bps) => {
2270+ if * bps == 0 || * bps > 10_000 {
2271+ return Err ( InsuranceError :: InvalidPayoutMode ) ;
2272+ }
2273+ remaining_coverage. saturating_mul ( * bps as u128 ) / 10_000
2274+ }
2275+ PayoutMode :: FullCoverage => remaining_coverage,
2276+ } ;
2277+ Ok ( amount)
2278+ }
2279+
2280+ fn ensure_payout_mode_valid ( mode : & PayoutMode ) -> Result < ( ) , InsuranceError > {
2281+ match mode {
2282+ PayoutMode :: Fixed ( v) => {
2283+ if * v == 0 {
2284+ return Err ( InsuranceError :: InvalidPayoutMode ) ;
2285+ }
2286+ }
2287+ PayoutMode :: PercentBps ( bps) => {
2288+ if * bps == 0 || * bps > 10_000 {
2289+ return Err ( InsuranceError :: InvalidPayoutMode ) ;
2290+ }
2291+ }
2292+ PayoutMode :: FullCoverage => { }
2293+ }
2294+ Ok ( ( ) )
2295+ }
2296+
19842297 fn try_reinsurance_recovery (
19852298 & mut self ,
19862299 claim_id : u64 ,
@@ -2077,6 +2390,9 @@ mod propchain_insurance {
20772390
20782391pub use crate :: propchain_insurance:: { InsuranceError , PropertyInsurance } ;
20792392
2393+ #[ cfg( test) ]
2394+ mod tests;
2395+
20802396// Unit tests extracted to tests.rs (Issue #101)
20812397#[ path = "tests.rs" ]
20822398mod insurance_tests_module;
0 commit comments