forked from InsurNiffy/niff-Stellar-shurance
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathpolicy.rs
More file actions
680 lines (603 loc) · 23 KB
/
Copy pathpolicy.rs
File metadata and controls
680 lines (603 loc) · 23 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
use crate::{
ledger, premium, storage, token,
types::{AgeBand, CoverageTier, Policy, PolicyType, PremiumQuote, RegionTier, RiskInput},
validate::{self, Error},
};
use soroban_sdk::{contracterror, contractevent, contracttype, Address, Env, String};
pub use ledger::QUOTE_TTL_LEDGERS;
/// Current event schema version.
pub const POLICY_EVENT_VERSION: u32 = 1;
#[contracterror]
#[derive(Copy, Clone, Debug, Eq, PartialEq, PartialOrd, Ord)]
#[repr(u32)]
pub enum PolicyError {
/// Contract is paused by admin.
ContractPaused = 100,
/// A policy with this (holder, policy_id) already exists.
DuplicatePolicyId = 101,
/// Coverage must be > 0.
InvalidCoverage = 102,
/// Computed premium is zero or negative.
InvalidPremium = 103,
/// Premium computation overflowed.
PremiumOverflow = 104,
/// Policy duration would overflow ledger sequence.
LedgerOverflow = 105,
/// Policy struct failed internal validation.
PolicyValidation = 106,
/// Caller is not authorized.
Unauthorized = 107,
/// Age out of range (1..=120).
InvalidAge = 108,
/// Risk score out of range (0..=100).
InvalidRiskScore = 109,
/// Supplied asset is not on the admin-controlled allowlist.
AssetNotAllowed = 110,
/// Policy not found.
NotFound = 111,
/// Policy is already active.
AlreadyActive = 112,
/// Keeper `process_expired`: policy is not yet at `end_ledger`.
NotYetExpired = 113,
/// `renew_policy` called before the renewal window opens.
NotInRenewalWindow = 114,
/// `renew_policy`: policy is inactive (terminated or deactivated).
PolicyInactive = 115,
/// `renew_policy`: an open claim exists for this policy.
OpenClaimBlocksRenewal = 116,
/// `renew_policy`: strike count blocks renewal (see `STRIKE_DEACTIVATION_THRESHOLD`).
TooManyStrikesForRenewal = 117,
/// Reserved / legacy: expired renewals now return [`crate::types::RenewPolicyOutcome::Lapsed`] `Ok`.
Expired = 118,
}
#[contracttype]
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct QuoteFailure {
pub code: u32,
pub message: String,
}
/// Versioned event emitted by `initiate_policy`.
#[contractevent]
#[derive(Clone, Debug)]
pub struct PolicyInitiated {
#[topic]
pub holder: Address,
pub version: u32,
pub policy_id: u32,
pub premium: i128,
pub asset: Address,
pub policy_type: PolicyType,
pub region: RegionTier,
pub coverage: i128,
pub start_ledger: u32,
pub end_ledger: u32,
}
/// Emitted when the payout beneficiary is set or changed (including at policy initiation when non-empty).
#[contractevent]
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct BeneficiaryUpdated {
#[topic]
pub holder: Address,
#[topic]
pub policy_id: u32,
pub old_beneficiary: Option<Address>,
pub new_beneficiary: Option<Address>,
}
/// Event emitted by `renew_policy`.
#[contractevent]
#[derive(Clone, Debug)]
pub struct PolicyRenewed {
#[topic]
pub holder: Address,
pub version: u32,
pub policy_id: u32,
pub premium: i128,
pub new_end_ledger: u32,
}
/// Emitted at most once per `(holder, policy_id, end_ledger)` term when expiry is detected.
///
/// **Timing:** `reported_at_ledger` is the ledger of the transaction that observes expiry.
/// It may be **strictly greater** than `expiry_ledger` if no call ran exactly at expiry
/// (keeper delay is normal). Indexers and notification services should **deduplicate on
/// `policy_id`** (and holder) and must not assume the event fires on the expiry ledger itself.
///
/// **`renew_policy` on an expired policy:** the call returns [`crate::types::RenewPolicyOutcome::Lapsed`]
/// in **`Ok`** (not `Err`) so this event and idempotency storage are not rolled back.
#[contractevent(topics = ["niffyinsure", "policy_expired"])]
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct PolicyExpired {
#[topic]
pub holder: Address,
#[topic]
pub policy_id: u32,
pub expiry_ledger: u32,
pub reported_at_ledger: u32,
}
pub fn generate_premium(
env: &Env,
region: RegionTier,
age_band: AgeBand,
coverage_type: CoverageTier,
safety_score: u32,
base_amount: i128,
include_breakdown: bool,
) -> Result<PremiumQuote, validate::Error> {
let input = RiskInput {
region,
age_band,
coverage: coverage_type,
safety_score,
};
validate::check_risk_input(&input)?;
if base_amount <= 0 {
return Err(validate::Error::InvalidBaseAmount);
}
let table = storage::get_multiplier_table(env);
let computation = premium::compute_premium(&input, base_amount, &table)?;
let line_items = if include_breakdown {
Some(premium::build_line_items(env, &computation))
} else {
None
};
let current_ledger = env.ledger().sequence();
let valid_until_ledger = current_ledger
.checked_add(QUOTE_TTL_LEDGERS)
.ok_or(validate::Error::Overflow)?;
Ok(PremiumQuote {
total_premium: computation.total_premium,
line_items,
valid_until_ledger,
config_version: computation.config_version,
})
}
pub fn map_quote_error(env: &Env, err: Error) -> QuoteFailure {
let message = match err {
Error::InvalidBaseAmount => "invalid base amount: expected > 0",
Error::SafetyScoreOutOfRange => "invalid safety_score: expected 0..=100",
Error::InvalidConfigVersion => {
"invalid premium table version: expected a strictly newer version"
}
Error::MissingRegionMultiplier => "premium table missing one or more region multipliers",
Error::MissingAgeMultiplier => "premium table missing one or more age-band multipliers",
Error::MissingCoverageMultiplier => {
"premium table missing one or more coverage multipliers"
}
Error::RegionMultiplierOutOfBounds => {
"region multiplier out of bounds: expected 0.5000x..=5.0000x"
}
Error::AgeMultiplierOutOfBounds => {
"age-band multiplier out of bounds: expected 0.5000x..=5.0000x"
}
Error::CoverageMultiplierOutOfBounds => {
"coverage multiplier out of bounds: expected 0.5000x..=5.0000x"
}
Error::SafetyDiscountOutOfBounds => {
"safety discount out of bounds: expected 0.0000x..=0.5000x"
}
Error::Overflow => "pricing arithmetic overflow: reduce base amount or multiplier values",
Error::DivideByZero => "pricing divide by zero: check configured scaling factors",
Error::InvalidQuoteTtl => "quote ttl misconfigured: contact support",
Error::NegativePremiumNotSupported => "negative premium inputs are not supported",
Error::ClaimNotFound => "claim not found",
Error::InvalidAsset => "claim asset is not allowlisted for payout",
Error::InsufficientTreasury => "treasury balance is insufficient for the approved payout",
Error::AlreadyPaid => "claim payout already executed",
Error::ClaimNotApproved => "claim must be approved before payout",
Error::DuplicateOpenClaim => "an open claim already exists for this policy",
Error::ZeroCoverage => "policy coverage must be greater than zero",
Error::ZeroPremium => "policy premium must be greater than zero",
Error::InvalidLedgerWindow => {
"invalid ledger window: end_ledger must be greater than start_ledger"
}
Error::PolicyExpired => "policy is expired",
Error::PolicyInactive => "policy is inactive",
Error::ClaimAmountZero => "claim amount must be greater than zero",
Error::ClaimExceedsCoverage => "claim amount exceeds policy coverage",
Error::PolicyNotFound => "policy not found",
Error::ExcessiveEvidenceBytes => "claim evidence payload exceeds the configured size limit",
Error::DetailsTooLong => "claim details exceed maximum length",
Error::TooManyImageUrls => "too many image URLs supplied",
Error::ImageUrlTooLong => "image URL exceeds maximum length",
Error::ReasonTooLong => "termination reason exceeds maximum length",
Error::ClaimAlreadyTerminal => "claim already reached a terminal status",
Error::DuplicateVote => "duplicate vote detected",
Error::CalculatorNotSet => "no external calculator configured",
Error::CalculatorCallFailed => "cross-contract call to premium calculator failed",
Error::CalculatorPaused => "premium calculator is paused; policy bind rejected",
Error::VotingWindowClosed => "voting window has closed; use finalize_claim",
Error::VotingWindowStillOpen => "voting window is still open; cannot finalize yet",
Error::NotEligibleVoter => "caller is not in the claim voter snapshot",
Error::RateLimitExceeded => "claim rate-limit: wait before filing another claim",
Error::AppealWindowClosed => "appeal window has closed",
Error::AppealAlreadyOpen => "an appeal is already open for this claim",
Error::MaxAppealsReached => "claim has reached the maximum allowed appeals",
Error::ClaimNotRejected => "claim is not in rejected status; cannot open appeal",
Error::AppealNotOpen => "no appeal is currently open",
Error::AppealWindowStillOpen => "appeal voting window is still open; cannot finalize yet",
Error::VotingDurationOutOfBounds => {
"voting duration ledgers outside allowed min/max; see contract docs"
}
Error::PolicyBatchTooLarge => "batch exceeds maximum allowed keys per call",
};
QuoteFailure {
code: err as u32,
message: String::from_str(env, message),
}
}
/// Turns an accepted quote into an enforceable on-chain policy.
///
/// # Asset
/// `asset` must be on the admin-controlled allowlist at call time.
/// The asset is bound to the policy and used for both premium payment
/// and future claim payouts — no cross-asset settlement in MVP.
#[allow(clippy::too_many_arguments)]
pub fn initiate_policy(
env: &Env,
holder: Address,
policy_type: PolicyType,
region: RegionTier,
age_band: AgeBand,
coverage_type: CoverageTier,
safety_score: u32,
base_amount: i128,
asset: Address,
beneficiary: Option<Address>,
) -> Result<Policy, PolicyError> {
// Check granular pause: policy binding should be blocked if bind_paused
storage::assert_bind_not_paused(env);
// Asset allowlist check — before auth so callers get a clear error.
if !storage::is_allowed_asset(env, &asset) {
return Err(PolicyError::AssetNotAllowed);
}
holder.require_auth();
let input = RiskInput {
region: region.clone(),
age_band: age_band.clone(),
coverage: coverage_type,
safety_score,
};
if safety_score > 100 {
return Err(PolicyError::InvalidRiskScore);
}
if base_amount <= 0 {
return Err(PolicyError::InvalidCoverage);
}
// Compute premium via the calculator (external or local fallback).
let quote =
crate::calculator::compute_quote(env, &input, base_amount, false, QUOTE_TTL_LEDGERS)
.map_err(|e| match e {
validate::Error::CalculatorPaused => PolicyError::ContractPaused,
validate::Error::CalculatorCallFailed | validate::Error::CalculatorNotSet => {
PolicyError::PremiumOverflow
}
_ => PolicyError::PremiumOverflow,
})?;
let premium_amount = quote.total_premium;
if premium_amount <= 0 {
return Err(PolicyError::InvalidPremium);
}
// Allocate unique per-holder policy_id.
let policy_id = storage::next_policy_id(env, &holder);
if storage::has_policy(env, &holder, policy_id) {
return Err(PolicyError::DuplicatePolicyId);
}
// Premium transfer: holder -> treasury using the policy's bound asset.
// Done BEFORE any durable writes so failure leaves no partial state.
token::collect_premium(env, &holder, &asset, premium_amount);
let current_ledger = env.ledger().sequence();
let end_ledger = current_ledger
.checked_add(ledger::POLICY_DURATION_LEDGERS)
.ok_or(PolicyError::LedgerOverflow)?;
let policy = Policy {
holder: holder.clone(),
policy_id,
policy_type: policy_type.clone(),
region: region.clone(),
premium: premium_amount,
coverage: base_amount,
is_active: true,
start_ledger: current_ledger,
end_ledger,
asset: asset.clone(),
beneficiary: beneficiary.clone(),
terminated_at_ledger: 0,
termination_reason: crate::types::TerminationReason::None,
terminated_by_admin: false,
strike_count: 0,
};
validate::check_policy(&policy).map_err(|_| PolicyError::PolicyValidation)?;
storage::set_policy(env, &holder, policy_id, &policy);
storage::add_voter(env, &holder);
PolicyInitiated {
version: POLICY_EVENT_VERSION,
policy_id,
holder: holder.clone(),
premium: premium_amount,
asset: asset.clone(),
policy_type,
region,
coverage: base_amount,
start_ledger: current_ledger,
end_ledger,
}
.publish(env);
if let Some(ref b) = beneficiary {
BeneficiaryUpdated {
holder: holder.clone(),
policy_id,
old_beneficiary: None,
new_beneficiary: Some(b.clone()),
}
.publish(env);
}
Ok(policy)
}
/// Update the optional payout beneficiary. Only the policy holder may call (authenticated via `holder`).
///
/// Admin cannot change this without the holder signing the transaction.
pub fn set_beneficiary(
env: &Env,
holder: Address,
policy_id: u32,
new_beneficiary: Option<Address>,
) -> Result<(), PolicyError> {
holder.require_auth();
let mut policy = storage::get_policy(env, &holder, policy_id).ok_or(PolicyError::NotFound)?;
if policy.holder != holder {
return Err(PolicyError::Unauthorized);
}
let old_beneficiary = policy.beneficiary.clone();
if old_beneficiary == new_beneficiary {
return Ok(());
}
policy.beneficiary = new_beneficiary.clone();
validate::check_policy(&policy).map_err(|_| PolicyError::PolicyValidation)?;
storage::set_policy(env, &holder, policy_id, &policy);
BeneficiaryUpdated {
holder: holder.clone(),
policy_id,
old_beneficiary,
new_beneficiary,
}
.publish(env);
Ok(())
}
// ── Grace period admin setter ─────────────────────────────────────────────────
#[contractevent(topics = ["niffyinsure", "grace_period_updated"])]
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct GracePeriodUpdated {
pub old_ledgers: u32,
pub new_ledgers: u32,
}
pub fn set_grace_period_ledgers(env: &Env, ledgers: u32) -> Result<(), RenewalError> {
crate::admin::require_admin(env);
if !ledger::is_valid_grace_period_ledgers(ledgers) {
return Err(RenewalError::GracePeriodOutOfBounds);
}
let old = storage::get_grace_period_ledgers(env);
storage::set_grace_period_ledgers(env, ledgers);
GracePeriodUpdated {
old_ledgers: old,
new_ledgers: ledgers,
}
.publish(env);
Ok(())
}
pub fn get_grace_period_ledgers(env: &Env) -> u32 {
storage::get_grace_period_ledgers(env)
}
// ── renew_policy ──────────────────────────────────────────────────────────────
#[contracterror]
#[derive(Copy, Clone, Debug, Eq, PartialEq, PartialOrd, Ord)]
#[repr(u32)]
pub enum RenewalError {
/// Policy not found.
NotFound = 200,
/// Policy is not active.
Inactive = 201,
/// Current ledger is outside the renewal + grace window.
WindowClosed = 202,
/// An open claim is blocking renewal.
OpenClaimBlocking = 203,
/// Premium computation failed.
PremiumError = 204,
/// Ledger arithmetic overflow.
LedgerOverflow = 205,
/// Grace period value outside allowed [min, max] range.
GracePeriodOutOfBounds = 206,
}
/// Renew an existing active policy.
///
/// Eligible window: `[end - RENEWAL_WINDOW_LEDGERS, end + grace_period_ledgers)`.
/// Renewal within the grace period succeeds with no coverage gap — the new
/// term starts at `old_end_ledger + 1`.
/// Blocked when an open claim exists on the policy (mirrors the open-claim rule).
pub fn renew_policy(
env: &Env,
holder: Address,
policy_id: u32,
age_band: crate::types::AgeBand,
coverage_type: CoverageTier,
safety_score: u32,
base_amount: i128,
) -> Result<Policy, RenewalError> {
storage::assert_bind_not_paused(env);
holder.require_auth();
let mut policy = storage::get_policy(env, &holder, policy_id)
.ok_or(RenewalError::NotFound)?;
if !policy.is_active {
return Err(RenewalError::Inactive);
}
// Open-claim guard (mirrors the rule enforced in the backend).
if storage::has_open_claim(env, &holder, policy_id) {
return Err(RenewalError::OpenClaimBlocking);
}
let now = env.ledger().sequence();
let grace = storage::get_grace_period_ledgers(env);
if !ledger::is_in_renewal_window_with_grace(
now,
policy.end_ledger,
ledger::RENEWAL_WINDOW_LEDGERS,
grace,
) {
return Err(RenewalError::WindowClosed);
}
// Recalculate premium with the same deterministic formula.
let input = RiskInput {
region: policy.region.clone(),
age_band,
coverage: coverage_type,
safety_score,
};
let quote = crate::calculator::compute_quote(
env,
&input,
base_amount,
false,
ledger::QUOTE_TTL_LEDGERS,
)
.map_err(|_| RenewalError::PremiumError)?;
let premium_amount = quote.total_premium;
if premium_amount <= 0 {
return Err(RenewalError::PremiumError);
}
// Collect premium before any state mutation.
token::collect_premium(env, &holder, &policy.asset, premium_amount);
// New term starts immediately after old end — no gap, no overlap.
let new_start = policy.end_ledger.saturating_add(1);
let new_end = new_start
.checked_add(ledger::POLICY_DURATION_LEDGERS)
.ok_or(RenewalError::LedgerOverflow)?;
policy.start_ledger = new_start;
policy.end_ledger = new_end;
policy.premium = premium_amount;
storage::set_policy(env, &holder, policy_id, &policy);
PolicyRenewed {
version: POLICY_EVENT_VERSION,
policy_id,
holder: holder.clone(),
premium: premium_amount,
new_end_ledger: new_end,
}
.publish(env);
Ok(policy)
}
/// Emit [`PolicyExpired`] if `now >= end_ledger` and we have not yet recorded an event for this term.
pub fn publish_policy_expired_if_due(env: &Env, policy: &Policy, now: u32) {
if !ledger::is_expired(now, policy.end_ledger) {
return;
}
if storage::get_policy_expired_event_end_ledger(env, &policy.holder, policy.policy_id)
== Some(policy.end_ledger)
{
return;
}
PolicyExpired {
holder: policy.holder.clone(),
policy_id: policy.policy_id,
expiry_ledger: policy.end_ledger,
reported_at_ledger: now,
}
.publish(env);
storage::set_policy_expired_event_end_ledger(
env,
&policy.holder,
policy.policy_id,
policy.end_ledger,
);
storage::bump_instance(env);
}
/// Keeper entrypoint: observe expiry for indexers / notification pipelines.
///
/// Reverts with [`PolicyError::NotYetExpired`] if `now < end_ledger`. If expiry was already
/// notified for this policy term, succeeds without emitting a duplicate event.
pub fn process_expired(env: &Env, holder: Address, policy_id: u32) -> Result<(), PolicyError> {
storage::bump_instance(env);
let policy = storage::get_policy(env, &holder, policy_id).ok_or(PolicyError::NotFound)?;
let now = env.ledger().sequence();
if !ledger::is_expired(now, policy.end_ledger) {
return Err(PolicyError::NotYetExpired);
}
publish_policy_expired_if_due(env, &policy, now);
Ok(())
}
/// Extend policy duration after premium payment (renewal window only).
///
/// If the policy is already expired, records [`PolicyExpired`] if not yet recorded for this
/// term, then returns [`crate::types::RenewPolicyOutcome::Lapsed`] in **`Ok`** so storage and
/// events persist (an `Err` would roll back the contract invocation).
#[allow(clippy::too_many_arguments)]
pub fn renew_policy(
env: &Env,
holder: Address,
policy_id: u32,
age_band: AgeBand,
coverage_type: CoverageType,
safety_score: u32,
) -> Result<crate::types::RenewPolicyOutcome, PolicyError> {
storage::assert_bind_not_paused(env);
holder.require_auth();
let mut policy = storage::get_policy(env, &holder, policy_id).ok_or(PolicyError::NotFound)?;
let now = env.ledger().sequence();
if ledger::is_expired(now, policy.end_ledger) {
publish_policy_expired_if_due(env, &policy, now);
return Ok(crate::types::RenewPolicyOutcome::Lapsed);
}
if !policy.is_active {
return Err(PolicyError::PolicyInactive);
}
if storage::has_open_claim(env, &holder, policy_id) {
return Err(PolicyError::OpenClaimBlocksRenewal);
}
if policy.strike_count >= STRIKE_DEACTIVATION_THRESHOLD {
return Err(PolicyError::TooManyStrikesForRenewal);
}
if !ledger::is_in_renewal_window(
now,
policy.end_ledger,
ledger::RENEWAL_WINDOW_LEDGERS,
) {
return Err(PolicyError::NotInRenewalWindow);
}
if safety_score > 100 {
return Err(PolicyError::InvalidRiskScore);
}
if !storage::is_allowed_asset(env, &policy.asset) {
return Err(PolicyError::AssetNotAllowed);
}
let input = RiskInput {
region: policy.region.clone(),
age_band: age_band.clone(),
coverage: coverage_type,
safety_score,
};
let quote =
crate::calculator::compute_quote(env, &input, policy.coverage, false, QUOTE_TTL_LEDGERS)
.map_err(|e| match e {
Error::CalculatorPaused => PolicyError::ContractPaused,
Error::CalculatorCallFailed | Error::CalculatorNotSet => PolicyError::PremiumOverflow,
_ => PolicyError::PremiumOverflow,
})?;
let premium_amount = quote.total_premium;
if premium_amount <= 0 {
return Err(PolicyError::InvalidPremium);
}
token::collect_premium(env, &holder, &policy.asset, premium_amount);
let new_end = policy
.end_ledger
.checked_add(ledger::POLICY_DURATION_LEDGERS)
.ok_or(PolicyError::LedgerOverflow)?;
policy.premium = premium_amount;
policy.end_ledger = new_end;
validate::check_policy(&policy).map_err(|_| PolicyError::PolicyValidation)?;
storage::set_policy(env, &holder, policy_id, &policy);
PolicyRenewed {
version: POLICY_EVENT_VERSION,
holder: holder.clone(),
policy_id,
premium: premium_amount,
new_end_ledger: new_end,
}
.publish(env);
Ok(crate::types::RenewPolicyOutcome::Renewed(policy))
}