diff --git a/heromodels/examples/governance_proposal_example/main.rs b/heromodels/examples/governance_proposal_example/main.rs index f9221d0..bfbe78a 100644 --- a/heromodels/examples/governance_proposal_example/main.rs +++ b/heromodels/examples/governance_proposal_example/main.rs @@ -2,7 +2,7 @@ use chrono::{Duration, Utc}; use heromodels::db::{Collection, Db}; -use heromodels::models::governance::{Proposal, ProposalStatus, VoteEventStatus}; +use heromodels::models::governance::{Ballot, Proposal, ProposalStatus, VoteEventStatus}; fn main() { println!("Governance Proposal Model Example\n"); @@ -99,6 +99,46 @@ fn main() { } println!(""); + // Example of voting with comments using the cast_vote_with_comment method + println!("Adding votes with comments..."); + + // User 7 votes for 'Approve Allocation' with a comment + proposal = proposal.cast_vote_with_comment( + Some(110), // ballot_id + 7, // user_id + 1, // chosen_option_id (Approve Allocation) + 80, // shares + "I strongly support this proposal because it aligns with our community values." + ); + + // User 8 votes for 'Reject Allocation' with a comment + proposal = proposal.cast_vote_with_comment( + Some(111), // ballot_id + 8, // user_id + 2, // chosen_option_id (Reject Allocation) + 60, // shares + "I have concerns about the allocation priorities." + ); + + println!("\nBallots with Comments:"); + for ballot in &proposal.ballots { + if let Some(comment) = &ballot.comment { + println!( + "- Ballot ID: {}, User ID: {}, Option ID: {}, Shares: {}", + ballot.base_data.id, ballot.user_id, ballot.vote_option_id, ballot.shares_count + ); + println!(" Comment: \"{}\"", comment); + } + } + + println!("\nUpdated Vote Counts After Comments:"); + for option in &proposal.options { + println!( + "- Option ID: {}, Text: '{}', Votes: {}", + option.id, option.text, option.count + ); + } + // Change proposal status proposal = proposal.change_proposal_status(ProposalStatus::Active); println!("Changed Proposal Status to: {:?}", proposal.status); @@ -176,6 +216,50 @@ fn main() { // User 40 (ineligible) tries to vote with auto-generated ballot ID private_proposal = private_proposal.cast_vote(None, 40, 1, 50); + // Example of voting with comments on a private proposal + println!("\nAdding votes with comments to private proposal..."); + + // User 20 (eligible) votes with a comment + private_proposal = private_proposal.cast_vote_with_comment( + Some(202), // ballot_id + 20, // user_id (eligible) + 1, // chosen_option_id + 75, // shares + "I support this restructuring plan with some reservations." + ); + + // User 30 (eligible) votes with a comment + private_proposal = private_proposal.cast_vote_with_comment( + Some(203), // ballot_id + 30, // user_id (eligible) + 2, // chosen_option_id + 90, // shares + "I believe we should reconsider the timing of these changes." + ); + + // User 40 (ineligible) tries to vote with a comment + private_proposal = private_proposal.cast_vote_with_comment( + Some(204), // ballot_id + 40, // user_id (ineligible) + 1, // chosen_option_id + 50, // shares + "This restructuring seems unnecessary." + ); + + println!("Eligible users 20 and 30 added votes with comments."); + println!("Ineligible user 40 attempted to vote with a comment (should be rejected)."); + + println!("\nPrivate Proposal Ballots with Comments:"); + for ballot in &private_proposal.ballots { + if let Some(comment) = &ballot.comment { + println!( + "- Ballot ID: {}, User ID: {}, Option ID: {}, Shares: {}", + ballot.base_data.id, ballot.user_id, ballot.vote_option_id, ballot.shares_count + ); + println!(" Comment: \"{}\"", comment); + } + } + println!("Private Proposal Vote Counts:"); for option in &private_proposal.options { println!( diff --git a/heromodels/src/models/governance/proposal.rs b/heromodels/src/models/governance/proposal.rs index 5a715ab..0440f89 100644 --- a/heromodels/src/models/governance/proposal.rs +++ b/heromodels/src/models/governance/proposal.rs @@ -73,6 +73,7 @@ pub struct Ballot { 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 { @@ -94,6 +95,7 @@ impl Ballot { user_id, vote_option_id, shares_count, + comment: None, } } } @@ -229,4 +231,65 @@ impl Proposal { 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 + } }