diff --git a/src/community_advisors/models/de.rs b/src/community_advisors/models/de.rs index 81911b5d..b9355f7a 100644 --- a/src/community_advisors/models/de.rs +++ b/src/community_advisors/models/de.rs @@ -1,7 +1,5 @@ use serde::Deserialize; -use vit_servicing_station_lib::db::models::community_advisors_reviews::ReviewTag; - #[derive(Deserialize)] pub struct AdvisorReviewRow { pub proposal_id: String, @@ -22,9 +20,30 @@ pub struct AdvisorReviewRow { #[serde(alias = "Auditability Rating")] pub auditability_rating: u8, #[serde(alias = "Excellent")] - pub excellent: u32, + excellent: bool, #[serde(alias = "Good")] - pub good: u32, + good: bool, +} + +pub enum ReviewScore { + Excellent, + Good, +} + +impl AdvisorReviewRow { + pub fn score(&self) -> ReviewScore { + match (self.excellent, self.good) { + (true, false) => ReviewScore::Excellent, + (false, true) => ReviewScore::Excellent, + _ => { + // This should never happen + panic!( + "Invalid combination of scores from assessor {} for proposal {}", + self.assessor, self.proposal_id + ) + } + } + } } #[cfg(test)] diff --git a/src/community_advisors/models/mod.rs b/src/community_advisors/models/mod.rs index 9038e083..a66adfdb 100644 --- a/src/community_advisors/models/mod.rs +++ b/src/community_advisors/models/mod.rs @@ -1,5 +1,5 @@ mod de; mod tags; -pub use de::AdvisorReviewRow; +pub use de::{AdvisorReviewRow, ReviewScore}; pub use tags::TagsMap; diff --git a/src/rewards/ca/funding.rs b/src/rewards/ca/funding.rs index ab5bb52b..e80bb994 100644 --- a/src/rewards/ca/funding.rs +++ b/src/rewards/ca/funding.rs @@ -25,13 +25,18 @@ impl FundSetting { pub fn total_funds(&self) -> Funds { self.total } + + #[inline] + pub fn funds_per_proposal(&self, number_of_proposals: u64) -> Funds { + self.total / Funds::from(number_of_proposals) + } } #[derive(Deserialize)] pub struct ProposalRewardSlots { - pub excellent_slots: usize, - pub good_slots: usize, - pub filled_slots: usize, + pub excellent_slots: u64, + pub good_slots: u64, + pub filled_slots: u64, } impl Default for ProposalRewardSlots { @@ -39,7 +44,7 @@ impl Default for ProposalRewardSlots { Self { excellent_slots: 12, good_slots: 4, - filled_slots: 35, + filled_slots: 36, } } } diff --git a/src/rewards/ca/lottery.rs b/src/rewards/ca/lottery.rs index 43f9051d..5f7c08c0 100644 --- a/src/rewards/ca/lottery.rs +++ b/src/rewards/ca/lottery.rs @@ -14,6 +14,6 @@ pub fn lottery_winner(distribution: &TicketsDistribution) -> Ca { .iter() .map(|(ca, &tickets)| (ca.clone(), tickets)) .collect(); - let mut dist = WeightedIndex::new(items.iter().map(|x| x.1)).unwrap(); + let dist = WeightedIndex::new(items.iter().map(|x| x.1)).unwrap(); items[dist.sample(&mut rng)].0.clone() } diff --git a/src/rewards/ca/mod.rs b/src/rewards/ca/mod.rs index f306f618..34ecebb3 100644 --- a/src/rewards/ca/mod.rs +++ b/src/rewards/ca/mod.rs @@ -1,7 +1,79 @@ mod funding; mod lottery; -use serde::Deserialize; +use crate::community_advisors::models::{AdvisorReviewRow, ReviewScore}; +use crate::rewards::ca::funding::{Funds, ProposalRewardSlots}; +use std::collections::HashMap; + +pub use funding::FundSetting; pub type Ca = String; pub type ProposalId = String; + +pub type ProposalsRewards = HashMap; +pub type CaRewards = HashMap; +pub type ProposalsReviews = HashMap>; + +enum ProposalRewardsState { + // Proposal has the exact quantity reviews to be rewarded + Exact, + // Proposal has less reviews as needed so some of the funds should go back into the rewards pool + Unfilled(Funds), + // + OverLoaded, +} + +fn proposal_rewards_state( + proposal_reviews: &[AdvisorReviewRow], + proposal_fund: Funds, + rewards_slots: &ProposalRewardSlots, +) -> ProposalRewardsState { + let filled_slots: u64 = proposal_reviews + .iter() + .map(|review| match review.score() { + ReviewScore::Excellent => rewards_slots.excellent_slots, + ReviewScore::Good => rewards_slots.good_slots, + }) + .sum(); + + if filled_slots < rewards_slots.filled_slots { + let unfilled_funds = + proposal_fund * (Funds::from(filled_slots) / Funds::from(rewards_slots.filled_slots)); + ProposalRewardsState::Unfilled(unfilled_funds) + } else if filled_slots > rewards_slots.filled_slots { + ProposalRewardsState::OverLoaded + } else { + ProposalRewardsState::Exact + } +} + +pub fn calculate_funds_per_proposal( + funding: &FundSetting, + proposal_reviews: &ProposalsReviews, + rewards_slots: &ProposalRewardSlots, +) -> ProposalsRewards { + let per_proposal_reward = funding.funds_per_proposal(proposal_reviews.len() as u64); + // proportionally split rewards + let mut rewards: ProposalsRewards = proposal_reviews + .keys() + .cloned() + .zip(std::iter::repeat(per_proposal_reward)) + .collect(); + + // check rewards and split extra until there is no more to split + let underbudget_funds: Funds = proposal_reviews + .iter() + .map(|(id, reviews)| { + match proposal_rewards_state(reviews, per_proposal_reward, rewards_slots) { + ProposalRewardsState::Unfilled(extra_funds) => extra_funds, + _ => Funds::from(0u64), + } + }) + .sum(); + + let underbudget_rewards = underbudget_funds / Funds::from(proposal_reviews.len() as u64); + rewards.values_mut().for_each(|v| { + *v += underbudget_rewards; + }); + rewards +}