Skip to content

Commit 8ab8cf6

Browse files
authored
Merge pull request #424 from RaymondAbiola/Add_claim_automation
Insurance: Add claim automation
2 parents 1f4e947 + 581ca35 commit 8ab8cf6

4 files changed

Lines changed: 319 additions & 5 deletions

File tree

Cargo.lock

Lines changed: 0 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

contracts/insurance/Cargo.toml

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,6 @@ ink = { version = "5.0.0", default-features = false }
1717
scale = { package = "parity-scale-codec", version = "3.6.9", default-features = false, features = ["derive"] }
1818
scale-info = { version = "2.10.0", default-features = false, features = ["derive"] }
1919
propchain-traits = { path = "../traits", default-features = false }
20-
propchain-contracts = { path = "../lib", default-features = false }
2120

2221
[dev-dependencies]
2322
ink_e2e = "5.0.0"
@@ -32,7 +31,6 @@ std = [
3231
"scale/std",
3332
"scale-info/std",
3433
"propchain-traits/std",
35-
"propchain-contracts/std",
3634
]
3735
ink-as-dependency = []
3836
e2e-tests = []

contracts/insurance/src/lib.rs

Lines changed: 317 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ use ink::storage::Mapping;
1414
mod 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

20782391
pub 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"]
20822398
mod insurance_tests_module;

contracts/insurance/src/tests.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,8 @@ mod insurance_tests {
55
use ink::env::{test, DefaultEnvironment};
66

77
use crate::propchain_insurance::{
8-
ClaimStatus, CoverageType, InsuranceError, PolicyStatus, PropertyInsurance,
8+
ClaimStatus, CoverageType, InsuranceError, PayoutMode, PolicyStatus, PropertyInsurance,
9+
TriggerComparator, TriggerMetric,
910
};
1011

1112
fn setup() -> PropertyInsurance {

0 commit comments

Comments
 (0)