diff --git a/pallet-logion-vote/src/lib.rs b/pallet-logion-vote/src/lib.rs index d11a169..d3f912c 100644 --- a/pallet-logion-vote/src/lib.rs +++ b/pallet-logion-vote/src/lib.rs @@ -32,6 +32,8 @@ pub enum BallotStatus { } pub type VoteId = u64; +pub type VoteCompleted = bool; +pub type VoteApproved = bool; #[frame_support::pallet] pub mod pallet { @@ -40,8 +42,10 @@ pub mod pallet { dispatch::DispatchResultWithPostInfo, pallet_prelude::*, }; + use frame_system::ensure_signed; use frame_system::pallet_prelude::OriginFor; use logion_shared::{IsLegalOfficer, LocValidity}; + use crate::BallotStatus::{NotVoted, VotedNo, VotedYes}; use super::*; #[pallet::config] @@ -78,13 +82,21 @@ pub mod pallet { #[pallet::generate_deposit(pub (super) fn deposit_event)] pub enum Event { /// Issued upon new Vote creation. [voteId, legalOfficers] - VoteCreated(VoteId, Vec) + VoteCreated(VoteId, Vec), + /// Issued upon new Vote creation. [voteId, ballot, completed, approved] + VoteUpdated(VoteId, Ballot, VoteCompleted, VoteApproved), } #[pallet::error] pub enum Error { /// Given LOC is not valid (not found, or not closed or void) or does not belong to vote requester. InvalidLoc, + /// Given vote does not exist. + VoteNotFound, + /// User is not allowed to vote on given vote. + NotAllowed, + /// User has already voted on given vote. + AlreadyVoted, } #[pallet::call] @@ -115,5 +127,63 @@ pub mod pallet { Err(Error::::InvalidLoc)? } } + + /// Vote. + #[pallet::weight(0)] + pub fn vote( + origin: OriginFor, + #[pallet::compact] vote_id: VoteId, + vote_yes: bool, + ) -> DispatchResultWithPostInfo { + let who = ensure_signed(origin)?; + + if !>::contains_key(&vote_id) { + Err(Error::::VoteNotFound)? + } else { + let vote = >::get(vote_id).unwrap(); + let option_ballot_index = vote.ballots.iter().position(|vote| vote.voter == who); + if option_ballot_index.is_none() { + Err(Error::::NotAllowed)? + } else { + let ballot_index = option_ballot_index.unwrap(); + if vote.ballots[ballot_index].status != NotVoted { + Err(Error::::AlreadyVoted)? + } + let status = match vote_yes { + true => VotedYes, + false => VotedNo + }; + >::mutate(vote_id, |vote| { + let mutable_vote = vote.as_mut().unwrap(); + mutable_vote.ballots[ballot_index].status = status.clone(); + }); + let (completed, approved) = Self::is_vote_completed(vote_id); + Self::deposit_event(Event::VoteUpdated( + vote_id, + Ballot { status: status.clone(), voter: who }, + completed, + approved) + ); + Ok(().into()) + } + } + } + } + + impl Pallet { + pub fn is_vote_completed(vote_id: VoteId) -> (VoteCompleted, VoteApproved) { + let vote = >::get(vote_id).unwrap(); + let not_voted = vote.ballots.iter().find(|ballot| ballot.status == NotVoted); + match not_voted { + Some(_) => (false, false), + None => { + let voted_no = vote.ballots.iter().find(|ballot| ballot.status == VotedNo); + match voted_no { + Some(_) => (true, false), + None => (true, true) + } + } + } + } } } diff --git a/pallet-logion-vote/src/tests.rs b/pallet-logion-vote/src/tests.rs index 7035712..66f798c 100644 --- a/pallet-logion-vote/src/tests.rs +++ b/pallet-logion-vote/src/tests.rs @@ -11,7 +11,8 @@ fn it_creates_vote() { new_test_ext().execute_with(|| { assert_empty_storage(); assert_ok!(LogionVote::create_vote_for_all_legal_officers(RuntimeOrigin::signed(LEGAL_OFFICER1), LOC_ID)); - assert_eq!(LogionVote::last_vote_id(), 1); + let vote_id = LogionVote::last_vote_id(); + assert_eq!(vote_id, 1); assert_eq!(LogionVote::votes(1), Some( Vote { loc_id: LOC_ID, @@ -21,6 +22,7 @@ fn it_creates_vote() { ] })); assert_eq!(LogionVote::votes(2), None); + assert_eq!(LogionVote::is_vote_completed(vote_id), (false, false)) }); } @@ -42,6 +44,96 @@ fn it_fails_to_create_vote_when_wrong_loc() { }); } +#[test] +fn it_votes_yes() { + new_test_ext().execute_with(|| { + assert_empty_storage(); + assert_ok!(LogionVote::create_vote_for_all_legal_officers(RuntimeOrigin::signed(LEGAL_OFFICER1), LOC_ID)); + let vote_id: u64 = LogionVote::last_vote_id(); + assert_ok!(LogionVote::vote(RuntimeOrigin::signed(LEGAL_OFFICER1), vote_id, true)); + assert_eq!(LogionVote::votes(vote_id), Some( + Vote { + loc_id: LOC_ID, + ballots: vec![ + Ballot { voter: LEGAL_OFFICER1, status: BallotStatus::VotedYes }, + Ballot { voter: LEGAL_OFFICER2, status: BallotStatus::NotVoted }, + ] + })); + assert_eq!(LogionVote::is_vote_completed(vote_id), (false, false)) + }); +} + +#[test] +fn it_votes_yes_and_no() { + new_test_ext().execute_with(|| { + assert_empty_storage(); + assert_ok!(LogionVote::create_vote_for_all_legal_officers(RuntimeOrigin::signed(LEGAL_OFFICER1), LOC_ID)); + let vote_id: u64 = LogionVote::last_vote_id(); + assert_ok!(LogionVote::vote(RuntimeOrigin::signed(LEGAL_OFFICER1), vote_id, true)); + assert_ok!(LogionVote::vote(RuntimeOrigin::signed(LEGAL_OFFICER2), vote_id, false)); + assert_eq!(LogionVote::votes(vote_id), Some( + Vote { + loc_id: LOC_ID, + ballots: vec![ + Ballot { voter: LEGAL_OFFICER1, status: BallotStatus::VotedYes }, + Ballot { voter: LEGAL_OFFICER2, status: BallotStatus::VotedNo }, + ] + })); + assert_eq!(LogionVote::is_vote_completed(vote_id), (true, false)) + }); +} + +#[test] +fn it_votes_yes_and_yes() { + new_test_ext().execute_with(|| { + assert_empty_storage(); + assert_ok!(LogionVote::create_vote_for_all_legal_officers(RuntimeOrigin::signed(LEGAL_OFFICER1), LOC_ID)); + let vote_id: u64 = LogionVote::last_vote_id(); + assert_ok!(LogionVote::vote(RuntimeOrigin::signed(LEGAL_OFFICER1), vote_id, true)); + assert_ok!(LogionVote::vote(RuntimeOrigin::signed(LEGAL_OFFICER2), vote_id, true)); + assert_eq!(LogionVote::votes(vote_id), Some( + Vote { + loc_id: LOC_ID, + ballots: vec![ + Ballot { voter: LEGAL_OFFICER1, status: BallotStatus::VotedYes }, + Ballot { voter: LEGAL_OFFICER2, status: BallotStatus::VotedYes }, + ] + })); + assert_eq!(LogionVote::is_vote_completed(vote_id), (true, true)) + }); +} + +#[test] +fn it_fails_to_vote_wrong_vote_id() { + new_test_ext().execute_with(|| { + assert_empty_storage(); + assert_ok!(LogionVote::create_vote_for_all_legal_officers(RuntimeOrigin::signed(LEGAL_OFFICER1), LOC_ID)); + let wrong_vote_id: u64 = LogionVote::last_vote_id() + 100; + assert_err!(LogionVote::vote(RuntimeOrigin::signed(LEGAL_OFFICER2), wrong_vote_id, false), Error::::VoteNotFound); + }); +} + +#[test] +fn it_fails_to_vote_wrong_voter() { + new_test_ext().execute_with(|| { + assert_empty_storage(); + assert_ok!(LogionVote::create_vote_for_all_legal_officers(RuntimeOrigin::signed(LEGAL_OFFICER1), LOC_ID)); + let vote_id: u64 = LogionVote::last_vote_id(); + assert_err!(LogionVote::vote(RuntimeOrigin::signed(WALLET_USER), vote_id, true), Error::::NotAllowed); + }); +} + +#[test] +fn it_fails_to_vote_twice() { + new_test_ext().execute_with(|| { + assert_empty_storage(); + assert_ok!(LogionVote::create_vote_for_all_legal_officers(RuntimeOrigin::signed(LEGAL_OFFICER1), LOC_ID)); + let vote_id: u64 = LogionVote::last_vote_id(); + assert_ok!(LogionVote::vote(RuntimeOrigin::signed(LEGAL_OFFICER2), vote_id, false)); + assert_err!(LogionVote::vote(RuntimeOrigin::signed(LEGAL_OFFICER2), vote_id, true), Error::::AlreadyVoted); + }); +} + fn assert_empty_storage() { assert_eq!(LogionVote::votes(0), None); assert_eq!(LogionVote::votes(1), None);