// heromodels/src/models/governance/activity.rs use chrono::{DateTime, Utc}; use heromodels_derive::model; use rhai::{CustomType, TypeBuilder}; use serde::{Deserialize, Serialize}; // use std::collections::HashMap; use heromodels_core::BaseModelData; /// ActivityType defines the different types of governance activities that can be tracked #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] pub enum ActivityType { ProposalCreated, ProposalStatusChanged, VotingStarted, VotingEnded, VoteCast, VoteOptionAdded, ProposalDeleted, Custom, } impl Default for ActivityType { fn default() -> Self { ActivityType::Custom } } /// ActivityStatus defines the status of an activity #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] pub enum ActivityStatus { Pending, // Activity is scheduled but not yet executed InProgress, // Activity is currently being executed Completed, // Activity has been successfully completed Failed, // Activity failed to complete Cancelled, // Activity was cancelled before completion } impl Default for ActivityStatus { fn default() -> Self { ActivityStatus::Completed } } /// GovernanceActivity represents a single activity or event in the governance system /// This model tracks all significant actions and changes for audit and transparency purposes #[derive(Debug, Clone, Serialize, Deserialize, CustomType)] #[model] pub struct GovernanceActivity { pub base_data: BaseModelData, /// Type of activity that occurred pub activity_type: ActivityType, /// Status of the activity pub status: ActivityStatus, /// ID of the user who initiated this activity (if applicable) pub actor_id: Option, /// Name of the user who initiated this activity (for display purposes) pub actor_name: Option, /// ID of the target object (e.g., proposal_id, ballot_id, etc.) pub target_id: Option, /// Type of the target object (e.g., "proposal", "ballot", "vote_option") pub target_type: Option, /// Title or brief description of the activity pub title: String, /// Detailed description of what happened pub description: String, /// Additional metadata as a simple string pub metadata: String, /// When the activity occurred pub occurred_at: DateTime, /// When the activity was recorded in the system pub recorded_at: DateTime, /// Optional reference to related activities pub related_activity_ids: Vec, /// Tags for categorization and filtering pub tags: Vec, /// Severity level of the activity (for filtering and alerting) pub severity: ActivitySeverity, /// Whether this activity should be publicly visible pub is_public: bool, /// Optional expiration date for temporary activities pub expires_at: Option>, } /// ActivitySeverity defines the importance level of an activity #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] pub enum ActivitySeverity { Low, // Routine activities Normal, // Standard activities High, // Important activities Critical, // Critical activities that require attention } impl Default for ActivitySeverity { fn default() -> Self { ActivitySeverity::Normal } } impl GovernanceActivity { /// Create a new governance activity with auto-generated ID /// /// # Arguments /// * `id` - Optional ID for the activity (use None for auto-generated ID) /// * `activity_type` - Type of activity that occurred /// * `title` - Brief title of the activity /// * `description` - Detailed description of the activity /// * `actor_id` - Optional ID of the user who initiated the activity /// * `actor_name` - Optional name of the user who initiated the activity pub fn new( id: Option, activity_type: ActivityType, title: impl ToString, description: impl ToString, actor_id: Option, actor_name: Option, ) -> Self { let mut base_data = BaseModelData::new(); if let Some(id) = id { base_data.update_id(id); } let now = Utc::now(); Self { base_data, activity_type, status: ActivityStatus::Completed, actor_id: actor_id.map(|id| id.to_string()), actor_name: actor_name.map(|name| name.to_string()), target_id: None, target_type: None, title: title.to_string(), description: description.to_string(), metadata: String::new(), occurred_at: now, recorded_at: now, related_activity_ids: Vec::new(), tags: Vec::new(), severity: ActivitySeverity::Normal, is_public: true, expires_at: None, } } /// Set the target of this activity pub fn with_target(mut self, target_id: u32, target_type: impl ToString) -> Self { self.target_id = Some(target_id); self.target_type = Some(target_type.to_string()); self } /// Set the status of this activity pub fn with_status(mut self, status: ActivityStatus) -> Self { self.status = status; self } /// Set the severity of this activity pub fn with_severity(mut self, severity: ActivitySeverity) -> Self { self.severity = severity; self } /// Set the occurred_at timestamp pub fn with_occurred_at(mut self, occurred_at: DateTime) -> Self { self.occurred_at = occurred_at; self } /// Add metadata to this activity pub fn with_metadata(mut self, key: impl ToString, value: impl ToString) -> Self { if !self.metadata.is_empty() { self.metadata.push_str(", "); } self.metadata .push_str(&format!("{}={}", key.to_string(), value.to_string())); self } /// Add a tag to this activity pub fn with_tag(mut self, tag: impl ToString) -> Self { let tag_str = tag.to_string(); if !self.tags.contains(&tag_str) { self.tags.push(tag_str); } self } /// Add multiple tags to this activity pub fn with_tags(mut self, tags: Vec) -> Self { for tag in tags { if !self.tags.contains(&tag) { self.tags.push(tag); } } self } /// Set whether this activity is public pub fn with_visibility(mut self, is_public: bool) -> Self { self.is_public = is_public; self } /// Set an expiration date for this activity pub fn with_expiration(mut self, expires_at: DateTime) -> Self { self.expires_at = Some(expires_at); self } /// Add a related activity ID pub fn with_related_activity(mut self, activity_id: u32) -> Self { if !self.related_activity_ids.contains(&activity_id) { self.related_activity_ids.push(activity_id); } self } /// Check if this activity has expired pub fn is_expired(&self) -> bool { if let Some(expires_at) = self.expires_at { Utc::now() > expires_at } else { false } } /// Get a formatted summary of this activity pub fn summary(&self) -> String { format!( "[{}] {} - {} (by {})", self.occurred_at.format("%Y-%m-%d %H:%M:%S UTC"), self.title, self.description, self.actor_name.as_deref().unwrap_or("System") ) } } /// Factory methods for creating common governance activities impl GovernanceActivity { /// Create an activity for proposal creation pub fn proposal_created( proposal_id: u32, proposal_title: impl ToString, creator_id: impl ToString, creator_name: impl ToString, ) -> Self { let creator_name_str = creator_name.to_string(); let proposal_title_str = proposal_title.to_string(); Self::new( None, ActivityType::ProposalCreated, format!("{} created a new proposal", creator_name_str), format!( "{} created a new proposal titled '{}' for community consideration", creator_name_str, proposal_title_str ), Some(creator_id), Some(creator_name_str), ) .with_target(proposal_id, "proposal") .with_metadata("proposal_title", proposal_title_str) .with_tag("proposal") .with_tag("creation") .with_severity(ActivitySeverity::Normal) } /// Create an activity for proposal status change pub fn proposal_status_changed( proposal_id: u32, proposal_title: impl ToString, old_status: impl ToString, new_status: impl ToString, actor_id: Option, actor_name: Option, ) -> Self { let proposal_title_str = proposal_title.to_string(); let old_status_str = old_status.to_string(); let new_status_str = new_status.to_string(); let actor_name_str = actor_name .map(|n| n.to_string()) .unwrap_or_else(|| "System".to_string()); Self::new( None, ActivityType::ProposalStatusChanged, format!( "{} changed proposal status to {}", actor_name_str, new_status_str ), format!( "{} changed the status of proposal '{}' from {} to {}", actor_name_str, proposal_title_str, old_status_str, new_status_str ), actor_id, Some(actor_name_str), ) .with_target(proposal_id, "proposal") .with_metadata("proposal_title", proposal_title_str) .with_metadata("old_status", old_status_str) .with_metadata("new_status", new_status_str) .with_tag("proposal") .with_tag("status_change") .with_severity(ActivitySeverity::High) } /// Create an activity for vote casting pub fn vote_cast( proposal_id: u32, proposal_title: impl ToString, ballot_id: u32, voter_id: impl ToString, voter_name: impl ToString, option_text: impl ToString, shares: i64, ) -> Self { let voter_name_str = voter_name.to_string(); let option_text_str = option_text.to_string(); let proposal_title_str = proposal_title.to_string(); // Create a more natural vote description let vote_description = if option_text_str.to_lowercase().contains("approve") || option_text_str.to_lowercase().contains("yes") { format!( "{} voted YES on proposal '{}'", voter_name_str, proposal_title_str ) } else if option_text_str.to_lowercase().contains("reject") || option_text_str.to_lowercase().contains("no") { format!( "{} voted NO on proposal '{}'", voter_name_str, proposal_title_str ) } else if option_text_str.to_lowercase().contains("abstain") { format!( "{} abstained from voting on proposal '{}'", voter_name_str, proposal_title_str ) } else { format!( "{} voted '{}' on proposal '{}'", voter_name_str, option_text_str, proposal_title_str ) }; Self::new( None, ActivityType::VoteCast, format!("{} submitted a vote", voter_name_str), format!("{} with {} voting shares", vote_description, shares), Some(voter_id), Some(voter_name_str), ) .with_target(proposal_id, "proposal") .with_metadata("ballot_id", ballot_id.to_string()) .with_metadata("option_text", option_text_str) .with_metadata("shares", shares.to_string()) .with_metadata("proposal_title", proposal_title_str) .with_tag("vote") .with_tag("ballot") .with_severity(ActivitySeverity::Normal) } /// Create an activity for voting period start pub fn voting_started( proposal_id: u32, proposal_title: impl ToString, start_date: DateTime, end_date: DateTime, ) -> Self { let proposal_title_str = proposal_title.to_string(); Self::new( None, ActivityType::VotingStarted, format!("Voting opened for '{}'", proposal_title_str), format!( "Community voting has opened for proposal '{}' and will close on {}", proposal_title_str, end_date.format("%B %d, %Y at %H:%M UTC") ), None::, Some("System"), ) .with_target(proposal_id, "proposal") .with_metadata("proposal_title", proposal_title_str) .with_metadata("start_date", start_date.to_rfc3339()) .with_metadata("end_date", end_date.to_rfc3339()) .with_tag("voting") .with_tag("period_start") .with_severity(ActivitySeverity::High) .with_occurred_at(start_date) } /// Create an activity for voting period end pub fn voting_ended( proposal_id: u32, proposal_title: impl ToString, total_votes: usize, total_shares: i64, ) -> Self { let proposal_title_str = proposal_title.to_string(); Self::new( None, ActivityType::VotingEnded, format!("Voting closed for '{}'", proposal_title_str), format!( "Community voting has ended for proposal '{}'. Final results: {} votes cast representing {} total voting shares", proposal_title_str, total_votes, total_shares ), None::, Some("System"), ) .with_target(proposal_id, "proposal") .with_metadata("proposal_title", proposal_title_str) .with_metadata("total_votes", total_votes.to_string()) .with_metadata("total_shares", total_shares.to_string()) .with_tag("voting") .with_tag("period_end") .with_severity(ActivitySeverity::High) } /// Create an activity for vote option addition pub fn vote_option_added( proposal_id: u32, proposal_title: impl ToString, option_id: u8, option_text: impl ToString, actor_id: Option, actor_name: Option, ) -> Self { let proposal_title_str = proposal_title.to_string(); let option_text_str = option_text.to_string(); let actor_name_str = actor_name .map(|n| n.to_string()) .unwrap_or_else(|| "System".to_string()); Self::new( None, ActivityType::VoteOptionAdded, format!("{} added a voting option", actor_name_str), format!( "{} added the voting option '{}' to proposal '{}'", actor_name_str, option_text_str, proposal_title_str ), actor_id, Some(actor_name_str), ) .with_target(proposal_id, "proposal") .with_metadata("proposal_title", proposal_title_str) .with_metadata("option_id", option_id.to_string()) .with_metadata("option_text", option_text_str) .with_tag("vote_option") .with_tag("addition") .with_severity(ActivitySeverity::Normal) } /// Create an activity for proposal deletion pub fn proposal_deleted( proposal_id: u32, proposal_title: impl ToString, actor_id: impl ToString, actor_name: impl ToString, reason: Option, ) -> Self { let proposal_title_str = proposal_title.to_string(); let actor_name_str = actor_name.to_string(); let description = if let Some(reason) = reason { format!( "{} deleted proposal '{}'. Reason: {}", actor_name_str, proposal_title_str, reason.to_string() ) } else { format!( "{} deleted proposal '{}'", actor_name_str, proposal_title_str ) }; Self::new( None, ActivityType::ProposalDeleted, format!("{} deleted a proposal", actor_name_str), description, Some(actor_id), Some(actor_name_str), ) .with_target(proposal_id, "proposal") .with_metadata("proposal_title", proposal_title_str) .with_tag("proposal") .with_tag("deletion") .with_severity(ActivitySeverity::Critical) } }