523 lines
17 KiB
Rust
523 lines
17 KiB
Rust
// 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<String>,
|
|
|
|
/// Name of the user who initiated this activity (for display purposes)
|
|
pub actor_name: Option<String>,
|
|
|
|
/// ID of the target object (e.g., proposal_id, ballot_id, etc.)
|
|
pub target_id: Option<u32>,
|
|
|
|
/// Type of the target object (e.g., "proposal", "ballot", "vote_option")
|
|
pub target_type: Option<String>,
|
|
|
|
/// 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<Utc>,
|
|
|
|
/// When the activity was recorded in the system
|
|
pub recorded_at: DateTime<Utc>,
|
|
|
|
/// Optional reference to related activities
|
|
pub related_activity_ids: Vec<u32>,
|
|
|
|
/// Tags for categorization and filtering
|
|
pub tags: Vec<String>,
|
|
|
|
/// 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<DateTime<Utc>>,
|
|
}
|
|
|
|
/// 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<u32>,
|
|
activity_type: ActivityType,
|
|
title: impl ToString,
|
|
description: impl ToString,
|
|
actor_id: Option<impl ToString>,
|
|
actor_name: Option<impl ToString>,
|
|
) -> 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<Utc>) -> 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<String>) -> 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<Utc>) -> 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<impl ToString>,
|
|
actor_name: Option<impl ToString>,
|
|
) -> 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<Utc>,
|
|
end_date: DateTime<Utc>,
|
|
) -> 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::<String>,
|
|
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::<String>,
|
|
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<impl ToString>,
|
|
actor_name: Option<impl ToString>,
|
|
) -> 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<impl ToString>,
|
|
) -> 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)
|
|
}
|
|
}
|