// heromodels/src/models/governance/proposal.rs use chrono::{DateTime, Utc}; use heromodels_derive::model; // For #[model] use rhai::{CustomType, TypeBuilder}; use serde::{Deserialize, Serialize}; use super::AttachedFile; use crate::models::core::Comment; use heromodels_core::BaseModelData; // --- Enums --- /// ProposalStatus defines the lifecycle status of a governance proposal itself #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] pub enum ProposalStatus { Draft, // Proposal is being prepared Active, // Proposal is active Approved, // Proposal has been formally approved Rejected, // Proposal has been formally rejected Cancelled, // Proposal was cancelled } impl Default for ProposalStatus { fn default() -> Self { ProposalStatus::Draft } } /// VoteEventStatus represents the status of the voting process for a proposal #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] pub enum VoteEventStatus { Upcoming, // Voting is scheduled but not yet open Open, // Voting is currently open Closed, // Voting has finished Cancelled, // The voting event was cancelled } impl Default for VoteEventStatus { fn default() -> Self { VoteEventStatus::Upcoming } } // --- Structs --- /// VoteOption represents a specific choice that can be voted on #[derive(Debug, Clone, Serialize, Deserialize, CustomType, Default)] pub struct VoteOption { pub id: u8, // Simple identifier for this option pub text: String, // Descriptive text of the option pub count: i64, // How many votes this option has received pub min_valid: Option, // Optional: minimum votes needed, pub comment: Option, // Optional: comment } impl VoteOption { pub fn new(id: u8, text: impl ToString, comment: Option) -> Self { Self { id, text: text.to_string(), count: 0, min_valid: None, comment: comment.map(|c| c.to_string()), } } } /// Ballot represents an individual vote cast by a user #[derive(Debug, Clone, Serialize, Deserialize, CustomType, Default)] // Removed rhai_model_export macro as it's causing compilation errors #[model] // Has base.Base in V spec pub struct Ballot { pub base_data: BaseModelData, pub user_id: u32, // The ID of the user who cast this ballot pub vote_option_id: u8, // The 'id' of the VoteOption chosen pub shares_count: i64, // Number of shares/tokens/voting power pub comment: Option, // Optional comment from the voter } impl Ballot { /// Create a new ballot with auto-generated ID /// /// # Arguments /// * `id` - Optional ID for the ballot (use None for auto-generated ID) /// * `user_id` - ID of the user who cast this ballot /// * `vote_option_id` - ID of the vote option chosen /// * `shares_count` - Number of shares/tokens/voting power pub fn new(id: Option, user_id: u32, vote_option_id: u8, shares_count: i64) -> Self { let mut base_data = BaseModelData::new(); if let Some(id) = id { base_data.update_id(id); } Self { base_data, user_id, vote_option_id, shares_count, comment: None, } } } /// Proposal represents a governance proposal that can be voted upon. #[derive(Debug, Clone, Serialize, Deserialize, CustomType)] // Removed rhai_model_export macro as it's causing compilation errors #[model] // Has base.Base in V spec pub struct Proposal { pub base_data: BaseModelData, pub creator_id: String, // User ID of the proposal creator pub creator_name: String, // User name of the proposal creator pub title: String, pub description: String, pub status: ProposalStatus, // Voting event aspects pub vote_start_date: DateTime, pub vote_end_date: DateTime, pub vote_status: VoteEventStatus, pub options: Vec, pub ballots: Vec, // This will store actual Ballot structs pub private_group: Option>, // Optional list of eligible user IDs pub tags: Vec, pub comments: Vec, pub attached_files: Vec, pub urgency_score: Option, } impl Default for Proposal { fn default() -> Self { Self { base_data: BaseModelData::new(), creator_id: "".to_string(), creator_name: String::new(), // Added missing field title: "".to_string(), description: "".to_string(), status: ProposalStatus::Draft, // created_at and updated_at are now in base_data vote_start_date: Utc::now(), vote_end_date: Utc::now(), vote_status: VoteEventStatus::Upcoming, options: vec![], ballots: vec![], private_group: None, tags: vec![], comments: vec![], attached_files: vec![], urgency_score: None, } } } impl Proposal { /// Create a new proposal with auto-generated ID /// /// # Arguments /// * `id` - Optional ID for the proposal (use None for auto-generated ID) /// * `creator_id` - ID of the user who created the proposal /// * `title` - Title of the proposal /// * `description` - Description of the proposal /// * `vote_start_date` - Date when voting starts /// * `vote_end_date` - Date when voting ends pub fn new( id: Option, creator_id: impl ToString, creator_name: impl ToString, title: impl ToString, description: impl ToString, status: ProposalStatus, tags: Vec, urgency_score: Option, ) -> Self { let mut base_data = BaseModelData::new(); if let Some(id) = id { base_data.update_id(id); } Self { base_data, creator_id: creator_id.to_string(), creator_name: creator_name.to_string(), title: title.to_string(), description: description.to_string(), status, vote_start_date: Utc::now(), vote_end_date: Utc::now(), vote_status: VoteEventStatus::Upcoming, options: Vec::new(), ballots: Vec::new(), private_group: None, tags, comments: Vec::new(), attached_files: Vec::new(), urgency_score, } } pub fn add_option( mut self, option_id: u8, option_text: impl ToString, comment: Option, ) -> Self { let new_option = VoteOption::new(option_id, option_text, comment); self.options.push(new_option); self } pub fn cast_vote( mut self, ballot_id: Option, user_id: u32, chosen_option_id: u8, shares: i64, ) -> Self { if self.vote_status != VoteEventStatus::Open { eprintln!("Voting is not open for proposal '{}'", self.title); return self; } if !self.options.iter().any(|opt| opt.id == chosen_option_id) { eprintln!( "Chosen option ID {} does not exist for proposal '{}'", chosen_option_id, self.title ); return self; } if let Some(group) = &self.private_group { if !group.contains(&user_id) { eprintln!( "User {} is not eligible to vote on proposal '{}'", user_id, self.title ); return self; } } let new_ballot = Ballot::new(ballot_id, user_id, chosen_option_id, shares); self.ballots.push(new_ballot); if let Some(option) = self .options .iter_mut() .find(|opt| opt.id == chosen_option_id) { option.count += shares; } self } pub fn change_proposal_status(mut self, new_status: ProposalStatus) -> Self { self.status = new_status; self } pub fn change_vote_event_status(mut self, new_status: VoteEventStatus) -> Self { self.vote_status = new_status; self } /// Cast a vote with a comment /// /// # Arguments /// * `ballot_id` - Optional ID for the ballot (use None for auto-generated ID) /// * `user_id` - ID of the user who is casting the vote /// * `chosen_option_id` - ID of the vote option chosen /// * `shares` - Number of shares/tokens/voting power /// * `comment` - Comment from the voter explaining their vote pub fn cast_vote_with_comment( mut self, ballot_id: Option, user_id: u32, chosen_option_id: u8, shares: i64, comment: impl ToString, ) -> Self { // First check if voting is open if self.vote_status != VoteEventStatus::Open { eprintln!("Voting is not open for proposal '{}'", self.title); return self; } // Check if the option exists if !self.options.iter().any(|opt| opt.id == chosen_option_id) { eprintln!( "Chosen option ID {} does not exist for proposal '{}'", chosen_option_id, self.title ); return self; } // Check eligibility for private proposals if let Some(group) = &self.private_group { if !group.contains(&user_id) { eprintln!( "User {} is not eligible to vote on proposal '{}'", user_id, self.title ); return self; } } // Create a new ballot with the comment let mut new_ballot = Ballot::new(ballot_id, user_id, chosen_option_id, shares); new_ballot.comment = Some(comment.to_string()); // Add the ballot to the proposal self.ballots.push(new_ballot); // Update the vote count for the chosen option if let Some(option) = self .options .iter_mut() .find(|opt| opt.id == chosen_option_id) { option.count += shares; } self } } pub enum ActivityType { ProposalCreated, VoteCast, VotingStarted, VotingEnded, } impl ToString for ActivityType { fn to_string(&self) -> String { match self { ActivityType::ProposalCreated => "proposal_created", ActivityType::VoteCast => "vote_cast", ActivityType::VotingStarted => "voting_started", ActivityType::VotingEnded => "voting_ended", } .to_string() } } /// Represents a governance activity in the system #[derive(Debug, Clone, Serialize, Deserialize, CustomType)] #[rhai_model_export(db_type = "std::sync::Arc")] #[model] // Has base.Base in V spec pub struct Activity { /// Base model data pub base_data: BaseModelData, /// Type of activity (proposal_created, vote_cast, etc.) pub activity_type: String, /// ID of the related proposal pub proposal_id: u32, /// Title of the related proposal pub proposal_title: String, /// Name of the user who performed the action pub creator_name: String, /// Description of the activity pub description: String, /// Date and time when the activity was last updated pub updated_at: DateTime, /// Date and time when the activity was created pub created_at: DateTime, } impl Activity { /// Create a default instance pub fn default() -> Self { let base_data = BaseModelData::new(); let created_at = Utc::now(); let updated_at = created_at; Self { base_data, activity_type: String::new(), proposal_id: 0, proposal_title: String::new(), creator_name: String::new(), description: String::new(), updated_at, created_at, } } /// Creates a new governance activity pub fn new( id: Option, activity_type: &str, proposal_id: u32, proposal_title: &str, creator_name: &str, description: &str, ) -> Self { let mut base_data = BaseModelData::new(); if let Some(id) = id { base_data.update_id(id); } let created_at = Utc::now(); let updated_at = created_at; Self { base_data, activity_type: activity_type.to_string(), proposal_id, proposal_title: proposal_title.to_string(), creator_name: creator_name.to_string(), description: description.to_string(), updated_at, created_at, } } /// Creates a proposal creation activity pub fn proposal_created(proposal_id: u32, proposal_title: &str, creator_name: &str) -> Self { Self::new( None, &ActivityType::ProposalCreated.to_string(), proposal_id, proposal_title, creator_name, &format!("Proposal '{}' created by {}", proposal_title, creator_name), ) } /// Creates a vote cast activity pub fn vote_cast(proposal_id: u32, proposal_title: &str, voter_name: &str) -> Self { Self::new( None, &ActivityType::VoteCast.to_string(), proposal_id, proposal_title, voter_name, &format!("{} voted on proposal '{}'", voter_name, proposal_title), ) } // Create voting start activity pub fn voting_started(proposal_id: u32, proposal_title: &str) -> Self { Self::new( None, &ActivityType::VotingStarted.to_string(), proposal_id, proposal_title, "System", &format!("Voting started for proposal '{}'", proposal_title), ) } // Create voting ended activity pub fn voting_ended(proposal_id: u32, proposal_title: &str) -> Self { Self::new( None, &ActivityType::VotingEnded.to_string(), proposal_id, proposal_title, "System", &format!("Voting ended for proposal '{}'", proposal_title), ) } }