463 lines
14 KiB
Rust
463 lines
14 KiB
Rust
// 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<i64>, // Optional: minimum votes needed,
|
|
pub comment: Option<String>, // Optional: comment
|
|
}
|
|
|
|
impl VoteOption {
|
|
pub fn new(id: u8, text: impl ToString, comment: Option<impl ToString>) -> 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<String>, // 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<u32>, 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<Utc>,
|
|
pub vote_end_date: DateTime<Utc>,
|
|
pub vote_status: VoteEventStatus,
|
|
pub options: Vec<VoteOption>,
|
|
pub ballots: Vec<Ballot>, // This will store actual Ballot structs
|
|
pub private_group: Option<Vec<u32>>, // Optional list of eligible user IDs
|
|
|
|
pub tags: Vec<String>,
|
|
pub comments: Vec<Comment>,
|
|
pub attached_files: Vec<AttachedFile>,
|
|
pub urgency_score: Option<f32>,
|
|
}
|
|
|
|
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<u32>,
|
|
creator_id: impl ToString,
|
|
creator_name: impl ToString,
|
|
title: impl ToString,
|
|
description: impl ToString,
|
|
status: ProposalStatus,
|
|
tags: Vec<String>,
|
|
urgency_score: Option<f32>,
|
|
) -> 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<impl ToString>,
|
|
) -> 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<u32>,
|
|
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<u32>,
|
|
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<crate::db::hero::OurDB>")]
|
|
#[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<Utc>,
|
|
/// Date and time when the activity was created
|
|
pub created_at: DateTime<Utc>,
|
|
}
|
|
|
|
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<u32>,
|
|
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),
|
|
)
|
|
}
|
|
}
|