11use crate :: {
22 premium,
3- types:: { PolicyType , PremiumQuote , RegionTier } ,
3+ storage,
4+ token,
5+ types:: { Policy , PolicyType , PremiumQuote , RegionTier } ,
6+ validate,
47} ;
5- use soroban_sdk:: { contracterror, contracttype, Env , String } ;
8+ use soroban_sdk:: { contractevent , contracterror, contracttype, Address , Env , String } ;
69
710/// How long a quote stays valid (in ledgers) from generation time.
811pub const QUOTE_TTL_LEDGERS : u32 = 100 ;
912
13+ /// Default policy duration in ledgers (~30 days at 5s/ledger ≈ 518_400).
14+ pub const POLICY_DURATION_LEDGERS : u32 = 518_400 ;
15+
16+ /// Current event schema version for PolicyInitiated.
17+ pub const POLICY_EVENT_VERSION : u32 = 1 ;
18+
1019#[ contracterror]
1120#[ derive( Copy , Clone , Debug , Eq , PartialEq , PartialOrd , Ord ) ]
1221#[ repr( u32 ) ]
@@ -17,13 +26,65 @@ pub enum QuoteError {
1726 ArithmeticOverflow = 4 ,
1827}
1928
29+ /// Errors specific to policy initiation and lifecycle.
30+ #[ contracterror]
31+ #[ derive( Copy , Clone , Debug , Eq , PartialEq , PartialOrd , Ord ) ]
32+ #[ repr( u32 ) ]
33+ pub enum PolicyError {
34+ /// Contract is paused by admin.
35+ ContractPaused = 100 ,
36+ /// A policy with this (holder, policy_id) already exists.
37+ DuplicatePolicyId = 101 ,
38+ /// Coverage must be > 0.
39+ InvalidCoverage = 102 ,
40+ /// Computed premium is zero or negative (should not happen with valid inputs).
41+ InvalidPremium = 103 ,
42+ /// Premium computation overflowed.
43+ PremiumOverflow = 104 ,
44+ /// Policy duration would overflow ledger sequence.
45+ LedgerOverflow = 105 ,
46+ /// Policy struct failed internal validation.
47+ PolicyValidation = 106 ,
48+ /// Caller is not authorized (require_auth failed or wrong signer).
49+ Unauthorized = 107 ,
50+ /// Age out of range (1..=120).
51+ InvalidAge = 108 ,
52+ /// Risk score out of range (1..=10).
53+ InvalidRiskScore = 109 ,
54+ }
55+
2056#[ contracttype]
2157#[ derive( Clone , Debug , Eq , PartialEq ) ]
2258pub struct QuoteFailure {
2359 pub code : u32 ,
2460 pub message : String ,
2561}
2662
63+ /// Versioned event emitted by `initiate_policy`.
64+ ///
65+ /// NestJS indexers subscribe to this event to render dashboards without
66+ /// scanning entire storage. The `version` field allows the indexer consumer
67+ /// to be versioned alongside contract releases.
68+ ///
69+ /// Topic fields (`holder`) are indexed for efficient subscription filtering.
70+ /// Data fields are serialised as a map in the event body.
71+ #[ contractevent]
72+ #[ derive( Clone , Debug ) ]
73+ pub struct PolicyInitiated {
74+ /// Schema version; currently 1.
75+ #[ topic]
76+ pub holder : Address ,
77+ pub version : u32 ,
78+ pub policy_id : u32 ,
79+ pub premium : i128 ,
80+ pub asset : Address ,
81+ pub policy_type : PolicyType ,
82+ pub region : RegionTier ,
83+ pub coverage : i128 ,
84+ pub start_ledger : u32 ,
85+ pub end_ledger : u32 ,
86+ }
87+
2788pub fn generate_premium (
2889 env : & Env ,
2990 policy_type : PolicyType ,
@@ -78,3 +139,117 @@ pub fn map_quote_error(env: &Env, err: QuoteError) -> QuoteFailure {
78139 message : String :: from_str ( env, message) ,
79140 }
80141}
142+
143+ /// Turns an accepted quote into an enforceable on-chain policy.
144+ ///
145+ /// # Auth
146+ /// `holder.require_auth()` — only the policyholder may initiate.
147+ ///
148+ /// # Flow
149+ /// 1. Check contract is not paused.
150+ /// 2. Authenticate the holder.
151+ /// 3. Validate inputs (age, risk_score, coverage).
152+ /// 4. Compute premium via `premium::compute_premium_checked`.
153+ /// 5. Allocate a unique per-holder `policy_id` (idempotent: if a client
154+ /// retries after a failed tx the counter is only bumped on success).
155+ /// 6. Transfer premium from holder → contract address.
156+ /// 7. Persist the `Policy` struct with `is_active = true`.
157+ /// 8. Update voter registry (add holder, increment active-policy count).
158+ /// 9. Emit versioned `PolicyInitiated` event for NestJS indexers.
159+ ///
160+ /// All durable writes happen **after** the premium transfer so that a failed
161+ /// transfer leaves zero partial state (no policy, no voter entry).
162+ pub fn initiate_policy (
163+ env : & Env ,
164+ holder : Address ,
165+ policy_type : PolicyType ,
166+ region : RegionTier ,
167+ coverage : i128 ,
168+ age : u32 ,
169+ risk_score : u32 ,
170+ ) -> Result < Policy , PolicyError > {
171+ // 1. Pause guard
172+ if storage:: is_paused ( env) {
173+ return Err ( PolicyError :: ContractPaused ) ;
174+ }
175+
176+ // 2. Authenticate the holder
177+ holder. require_auth ( ) ;
178+
179+ // 3. Input validation
180+ if age == 0 || age > 120 {
181+ return Err ( PolicyError :: InvalidAge ) ;
182+ }
183+ if risk_score == 0 || risk_score > 10 {
184+ return Err ( PolicyError :: InvalidRiskScore ) ;
185+ }
186+ if coverage <= 0 {
187+ return Err ( PolicyError :: InvalidCoverage ) ;
188+ }
189+
190+ // 4. Compute premium (smallest units / stroops)
191+ let premium_amount = premium:: compute_premium_checked ( & policy_type, & region, age, risk_score)
192+ . ok_or ( PolicyError :: PremiumOverflow ) ?;
193+ if premium_amount <= 0 {
194+ return Err ( PolicyError :: InvalidPremium ) ;
195+ }
196+
197+ // 5. Allocate unique per-holder policy_id
198+ let policy_id = storage:: next_policy_id ( env, & holder) ;
199+
200+ // Enforce uniqueness (defensive — next_policy_id is monotonic, but guard
201+ // against any future code path that might manually set an id).
202+ if storage:: has_policy ( env, & holder, policy_id) {
203+ return Err ( PolicyError :: DuplicatePolicyId ) ;
204+ }
205+
206+ // 6. Premium transfer: holder → contract address
207+ // Done BEFORE any durable writes so failure leaves no partial state.
208+ let token_addr = storage:: get_token ( env) ;
209+ let contract_addr = env. current_contract_address ( ) ;
210+ token:: transfer ( env, & token_addr, & holder, & contract_addr, premium_amount) ;
211+
212+ // 7. Build and validate policy struct
213+ let current_ledger = env. ledger ( ) . sequence ( ) ;
214+ let end_ledger = current_ledger
215+ . checked_add ( POLICY_DURATION_LEDGERS )
216+ . ok_or ( PolicyError :: LedgerOverflow ) ?;
217+
218+ let policy = Policy {
219+ holder : holder. clone ( ) ,
220+ policy_id,
221+ policy_type : policy_type. clone ( ) ,
222+ region : region. clone ( ) ,
223+ premium : premium_amount,
224+ coverage,
225+ is_active : true ,
226+ start_ledger : current_ledger,
227+ end_ledger,
228+ } ;
229+
230+ // Run structural validation (coverage > 0, premium > 0, ledger window).
231+ validate:: check_policy ( & policy) . map_err ( |_| PolicyError :: PolicyValidation ) ?;
232+
233+ // 8. Persist policy
234+ storage:: set_policy ( env, & holder, policy_id, & policy) ;
235+
236+ // 9. Update voter registry
237+ storage:: add_voter ( env, & holder) ;
238+
239+ // 10. Emit versioned PolicyInitiated event
240+ PolicyInitiated {
241+ version : POLICY_EVENT_VERSION ,
242+ policy_id,
243+ holder : holder. clone ( ) ,
244+ premium : premium_amount,
245+ asset : token_addr,
246+ policy_type,
247+ region,
248+ coverage,
249+ start_ledger : current_ledger,
250+ end_ledger,
251+ }
252+ . publish ( env) ;
253+
254+ Ok ( policy)
255+ }
0 commit comments