Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions contracts/niffyinsure/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,14 @@ Record the contract ID and the SHA-256 from `make sha` in your release notes so
| `stellar contract deploy` fails | Wrong CLI version | `stellar --version` must be ≥ 21 |
| Tests fail with `no_std` errors | Running `cargo test --target wasm32` | Tests run on native; omit `--target` flag |

## Quote behavior (`generate_premium`)

- `generate_premium` is a quote-only entrypoint: it does not increment `claim_id`, mutate policy state, or transfer funds.
- The response is a structured `PremiumQuote` with `total_premium`, optional `line_items` (for UX), and `valid_until_ledger`.
- MVP does not emit quote events to reduce event spam, avoid accidental PII leakage, and stay within Soroban payload limits.
- Validation failures return typed error codes; API layers can map these using `quote_error_message(code)` for support-friendly messages.
- Off-chain quote caches must enforce `valid_until_ledger`: if admin-adjustable multipliers are introduced later, stale cached quotes must be discarded and re-simulated before bind.

## Module map

```
Expand Down
46 changes: 46 additions & 0 deletions contracts/niffyinsure/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,52 @@ impl NiffyInsure {
storage::set_token(&env, &token);
}

/// Pure quote path: reads config and computes premium only.
/// This entrypoint intentionally performs no persistent writes.
pub fn generate_premium(
env: Env,
policy_type: types::PolicyType,
region: types::RegionTier,
age: u32,
risk_score: u32,
include_breakdown: bool,
) -> Result<types::PremiumQuote, policy::QuoteError> {
policy::generate_premium(
&env,
policy_type,
region,
age,
risk_score,
include_breakdown,
)
}

/// Converts quote failure codes to support-friendly messages for API layers.
pub fn quote_error_message(env: Env, code: u32) -> policy::QuoteFailure {
let err = match code {
1 => policy::QuoteError::InvalidAge,
2 => policy::QuoteError::InvalidRiskScore,
3 => policy::QuoteError::InvalidQuoteTtl,
_ => policy::QuoteError::ArithmeticOverflow,
};
policy::map_quote_error(&env, err)
}

/// Read-only helper for monitoring state in tests / ops tooling.
pub fn get_claim_counter(env: Env) -> u64 {
storage::get_claim_counter(&env)
}

/// Read-only helper for monitoring state in tests / ops tooling.
pub fn get_policy_counter(env: Env, holder: Address) -> u32 {
storage::get_policy_counter(&env, &holder)
}

/// Read-only helper for monitoring state in tests / ops tooling.
pub fn has_policy(env: Env, holder: Address, policy_id: u32) -> bool {
storage::has_policy(&env, &holder, policy_id)
}

// ── Policy domain ────────────────────────────────────────────────────
// generate_premium, initiate_policy, renew_policy, terminate_policy
// implemented in policy.rs — issue: feat/policy-lifecycle
Expand Down
88 changes: 80 additions & 8 deletions contracts/niffyinsure/src/policy.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,80 @@
// Policy lifecycle methods will be implemented here and exposed via
// NiffyInsure contractimpl in lib.rs.
//
// Planned public functions:
// generate_premium(env, policy_type, age, risk_score) -> i128
// initiate_policy(env, holder, policy_id, policy_type, coverage, age, risk_score)
// renew_policy(env, holder, policy_id)
// terminate_policy(env, holder, policy_id, reason)
use crate::{
premium,
types::{PolicyType, PremiumQuote, RegionTier},
};
use soroban_sdk::{contracterror, contracttype, Env, String};

/// How long a quote stays valid (in ledgers) from generation time.
pub const QUOTE_TTL_LEDGERS: u32 = 100;

#[contracterror]
#[derive(Copy, Clone, Debug, Eq, PartialEq, PartialOrd, Ord)]
#[repr(u32)]
pub enum QuoteError {
InvalidAge = 1,
InvalidRiskScore = 2,
InvalidQuoteTtl = 3,
ArithmeticOverflow = 4,
}

#[contracttype]
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct QuoteFailure {
pub code: u32,
pub message: String,
}

pub fn generate_premium(
env: &Env,
policy_type: PolicyType,
region: RegionTier,
age: u32,
risk_score: u32,
include_breakdown: bool,
) -> Result<PremiumQuote, QuoteError> {
if age == 0 || age > 120 {
return Err(QuoteError::InvalidAge);
}
if risk_score == 0 || risk_score > 10 {
return Err(QuoteError::InvalidRiskScore);
}
if QUOTE_TTL_LEDGERS == 0 {
return Err(QuoteError::InvalidQuoteTtl);
}

let total = premium::compute_premium_checked(&policy_type, &region, age, risk_score)
.ok_or(QuoteError::ArithmeticOverflow)?;

let line_items = if include_breakdown {
Some(
premium::build_line_items(env, &policy_type, &region, age, risk_score)
.ok_or(QuoteError::ArithmeticOverflow)?,
)
} else {
None
};

let current_ledger = env.ledger().sequence();
let valid_until_ledger = current_ledger
.checked_add(QUOTE_TTL_LEDGERS)
.ok_or(QuoteError::ArithmeticOverflow)?;

Ok(PremiumQuote {
total_premium: total,
line_items,
valid_until_ledger,
})
}

pub fn map_quote_error(env: &Env, err: QuoteError) -> QuoteFailure {
let message = match err {
QuoteError::InvalidAge => "invalid age: expected 1..=120",
QuoteError::InvalidRiskScore => "invalid risk_score: expected 1..=10",
QuoteError::InvalidQuoteTtl => "quote ttl misconfigured: contact support",
QuoteError::ArithmeticOverflow => "pricing arithmetic overflow: contact support",
};
QuoteFailure {
code: err as u32,
message: String::from_str(env, message),
}
}
91 changes: 90 additions & 1 deletion contracts/niffyinsure/src/premium.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
use crate::types::{PolicyType, RegionTier};
use crate::types::{PolicyType, PremiumQuoteLineItem, RegionTier};
use soroban_sdk::{Env, String, Vec};

/// Base annual premium in stroops (1 XLM = 10_000_000 stroops).
#[allow(dead_code)]
Expand Down Expand Up @@ -32,3 +33,91 @@ pub fn compute_premium(
};
BASE * (type_factor + region_factor + age_factor + risk_score as i128) / 10
}

#[allow(dead_code)]
pub fn type_factor(policy_type: &PolicyType) -> i128 {
match policy_type {
PolicyType::Auto => 15,
PolicyType::Health => 20,
PolicyType::Property => 10,
}
}

#[allow(dead_code)]
pub fn region_factor(region: &RegionTier) -> i128 {
match region {
RegionTier::Low => 8,
RegionTier::Medium => 10,
RegionTier::High => 14,
}
}

#[allow(dead_code)]
pub fn age_factor(age: u32) -> i128 {
if age < 25 {
15
} else if age > 60 {
13
} else {
10
}
}

#[allow(dead_code)]
pub fn compute_premium_checked(
policy_type: &PolicyType,
region: &RegionTier,
age: u32,
risk_score: u32,
) -> Option<i128> {
let tf = type_factor(policy_type);
let rf = region_factor(region);
let af = age_factor(age);
let raw = tf
.checked_add(rf)?
.checked_add(af)?
.checked_add(risk_score as i128)?;
BASE.checked_mul(raw)?.checked_div(10)
}

#[allow(dead_code)]
pub fn build_line_items(
env: &Env,
policy_type: &PolicyType,
region: &RegionTier,
age: u32,
risk_score: u32,
) -> Option<Vec<PremiumQuoteLineItem>> {
let tf = type_factor(policy_type);
let rf = region_factor(region);
let af = age_factor(age);
let rsk = risk_score as i128;

let base_type = BASE.checked_mul(tf)?.checked_div(10)?;
let base_region = BASE.checked_mul(rf)?.checked_div(10)?;
let base_age = BASE.checked_mul(af)?.checked_div(10)?;
let base_risk = BASE.checked_mul(rsk)?.checked_div(10)?;

let mut items = Vec::new(env);
items.push_back(PremiumQuoteLineItem {
component: String::from_str(env, "type"),
factor: tf,
amount: base_type,
});
items.push_back(PremiumQuoteLineItem {
component: String::from_str(env, "region"),
factor: rf,
amount: base_region,
});
items.push_back(PremiumQuoteLineItem {
component: String::from_str(env, "age"),
factor: af,
amount: base_age,
});
items.push_back(PremiumQuoteLineItem {
component: String::from_str(env, "risk_score"),
factor: rsk,
amount: base_risk,
});
Some(items)
}
20 changes: 20 additions & 0 deletions contracts/niffyinsure/src/storage.rs
Original file line number Diff line number Diff line change
Expand Up @@ -60,3 +60,23 @@ pub fn next_claim_id(env: &Env) -> u64 {
env.storage().instance().set(&DataKey::ClaimCounter, &next);
next
}

pub fn get_claim_counter(env: &Env) -> u64 {
env.storage()
.instance()
.get(&DataKey::ClaimCounter)
.unwrap_or(0u64)
}

pub fn get_policy_counter(env: &Env, holder: &Address) -> u32 {
env.storage()
.persistent()
.get(&DataKey::PolicyCounter(holder.clone()))
.unwrap_or(0u32)
}

pub fn has_policy(env: &Env, holder: &Address, policy_id: u32) -> bool {
env.storage()
.persistent()
.has(&DataKey::Policy(holder.clone(), policy_id))
}
21 changes: 21 additions & 0 deletions contracts/niffyinsure/src/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -150,3 +150,24 @@ pub struct Claim {
pub approve_votes: u32,
pub reject_votes: u32,
}

/// Premium quote line item for UX display.
#[contracttype]
#[derive(Clone)]
pub struct PremiumQuoteLineItem {
pub component: String,
pub factor: i128,
pub amount: i128,
}

/// Structured quote response returned by `generate_premium`.
///
/// Field names and ordering are kept stable for SDK bindings consumed by
/// backend simulation services.
#[contracttype]
#[derive(Clone)]
pub struct PremiumQuote {
pub total_premium: i128,
pub line_items: Option<Vec<PremiumQuoteLineItem>>,
pub valid_until_ledger: u32,
}
Loading
Loading