Skip to content

Commit 7952d68

Browse files
authored
feat: added eReputation weighted calculation (#459)
* feat: added eReputation weighted calculation * chore: add logs to check weight * chore: fix ts error * chore: fix eVoting adapter * chore: fix erep pollId being null * chore: add migration * fix: refresh issue * chore: remove logs * fix: ui on evoting * fix: rep based normal voting * chore: fix build * chore: disable erep on ranked and private
1 parent 797ff55 commit 7952d68

File tree

10 files changed

+186
-86
lines changed

10 files changed

+186
-86
lines changed

platforms/eVoting/src/app/(app)/[id]/page.tsx

Lines changed: 111 additions & 63 deletions
Large diffs are not rendered by default.

platforms/eVoting/src/app/(app)/create/page.tsx

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -141,6 +141,17 @@ export default function CreatePoll() {
141141
}
142142
}, [watchedVotingWeight, watchedMode, setValue]);
143143

144+
// Prevent blind voting (private visibility) + eReputation weighted combination
145+
React.useEffect(() => {
146+
if (watchedVisibility === "private" && watchedVotingWeight === "ereputation") {
147+
// If private visibility is selected and user tries to select eReputation, force to 1p1v
148+
setValue("votingWeight", "1p1v");
149+
} else if (watchedVotingWeight === "ereputation" && watchedVisibility === "private") {
150+
// If eReputation is selected and user tries to select private visibility, force to public
151+
setValue("visibility", "public");
152+
}
153+
}, [watchedVisibility, watchedVotingWeight, setValue]);
154+
144155
const addOption = () => {
145156
const newOptions = [...options, ""];
146157
setOptions(newOptions);
@@ -552,7 +563,7 @@ export default function CreatePoll() {
552563
<Label className={`flex items-center cursor-pointer p-4 border-2 rounded-lg transition-all duration-200 ${
553564
watchedVotingWeight === "ereputation"
554565
? "border-(--crimson) bg-(--crimson) text-white"
555-
: watchedMode === "rank"
566+
: watchedMode === "rank" || watchedVisibility === "private"
556567
? "border-gray-300 bg-gray-100 opacity-50 cursor-not-allowed"
557568
: "border-gray-300 hover:border-gray-400"
558569
}`}>
@@ -561,7 +572,7 @@ export default function CreatePoll() {
561572
value="ereputation"
562573
{...register("votingWeight")}
563574
className="sr-only"
564-
disabled={watchedMode === "rank"}
575+
disabled={watchedMode === "rank" || watchedVisibility === "private"}
565576
/>
566577
<div className="flex items-center">
567578
<ChartLine className="w-6 h-6 mr-3" />
@@ -572,6 +583,8 @@ export default function CreatePoll() {
572583
<div className="text-sm opacity-90">
573584
{watchedMode === "rank"
574585
? "Not available with Rank Based Voting"
586+
: watchedVisibility === "private"
587+
? "Not available with Blind Voting"
575588
: "Votes weighted by eReputation"}
576589
</div>
577590
</div>
@@ -581,7 +594,7 @@ export default function CreatePoll() {
581594
</RadioGroup>
582595
{watchedVotingWeight === "ereputation" && (
583596
<p className="mt-2 text-sm text-gray-600">
584-
Votes will be weighted by each voter's eReputation score. The poll title will automatically include "(eReputation Weighted)".
597+
Votes will be weighted by each voter's eReputation score.
585598
</p>
586599
)}
587600
{errors.votingWeight && (

platforms/eVoting/src/app/(app)/page.tsx

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
"use client";
22

33
import Link from "next/link";
4-
import { Plus, Vote, BarChart3, LogOut, Eye, UserX, Search, ChevronLeft, ChevronRight } from "lucide-react";
4+
import { Plus, Vote, BarChart3, LogOut, Eye, UserX, Search, ChevronLeft, ChevronRight, ChartLine, CircleUser } from "lucide-react";
55
import { Button } from "@/components/ui/button";
66
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
77
import { Badge } from "@/components/ui/badge";
@@ -163,6 +163,11 @@ export default function Home() {
163163
>
164164
Visibility {getSortIcon("visibility")}
165165
</th>
166+
<th
167+
className="text-left py-3 px-4 font-medium text-gray-700 cursor-pointer hover:bg-gray-50"
168+
>
169+
Voting Weight
170+
</th>
166171
<th
167172
className="text-left py-3 px-4 font-medium text-gray-700 cursor-pointer hover:bg-gray-50"
168173
>
@@ -211,6 +216,15 @@ export default function Home() {
211216
)}
212217
</Badge>
213218
</td>
219+
<td className="py-3 px-4">
220+
<Badge variant={poll.votingWeight === "ereputation" ? "default" : "secondary"} className="text-xs">
221+
{poll.votingWeight === "ereputation" ? (
222+
<><ChartLine className="w-3 h-3 mr-1" />eReputation Weighted</>
223+
) : (
224+
<><CircleUser className="w-3 h-3 mr-1" />1P 1V</>
225+
)}
226+
</Badge>
227+
</td>
214228
<td className="py-3 px-4">
215229
{poll.group ? (
216230
<Badge variant="outline" className="text-xs">

platforms/eVoting/src/lib/pollApi.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -114,9 +114,10 @@ export interface VoterDetail {
114114
export interface PollResults {
115115
poll: Poll;
116116
totalVotes: number;
117+
totalWeightedVotes?: number;
117118
totalEligibleVoters?: number;
118119
turnout?: number;
119-
mode?: "normal" | "point" | "rank";
120+
mode?: "normal" | "point" | "rank" | "ereputation";
120121
results: PollResultOption[];
121122
irvDetails?: IRVDetails;
122123
voterDetails?: VoterDetail[];

platforms/evoting-api/src/controllers/WebhookController.ts

Lines changed: 4 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -214,11 +214,7 @@ export class WebhookController {
214214
const pollIdValue = local.data.pollId.includes("(")
215215
? local.data.pollId.split("(")[1].split(")")[0]
216216
: local.data.pollId;
217-
pollId = await this.adapter.mappingDb.getLocalId(pollIdValue);
218-
if (!pollId) {
219-
console.error("Poll not found for globalId:", pollIdValue);
220-
return res.status(400).send();
221-
}
217+
pollId = pollIdValue;
222218
}
223219

224220
// Resolve groupId from global to local ID
@@ -235,8 +231,8 @@ export class WebhookController {
235231
}
236232
}
237233

238-
if (!pollId || !groupId) {
239-
console.error("Missing pollId or groupId:", { pollId, groupId });
234+
if (!pollId) {
235+
console.error("Missing pollId:", { pollId, groupId });
240236
return res.status(400).send();
241237
}
242238

@@ -258,7 +254,7 @@ export class WebhookController {
258254
// Create new result
259255
const newResult = voteReputationResultRepository.create({
260256
pollId: pollId,
261-
groupId: groupId,
257+
groupId: groupId || null,
262258
results: results
263259
});
264260

platforms/evoting-api/src/database/entities/VoteReputationResult.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -28,12 +28,12 @@ export class VoteReputationResult {
2828
@Column("uuid")
2929
pollId!: string;
3030

31-
@ManyToOne(() => Group)
31+
@ManyToOne(() => Group, { nullable: true })
3232
@JoinColumn({ name: "groupId" })
33-
group!: Group;
33+
group!: Group | null;
3434

35-
@Column("uuid")
36-
groupId!: string;
35+
@Column("uuid", { nullable: true })
36+
groupId!: string | null;
3737

3838
/**
3939
* Array of reputation scores for each group member
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import { MigrationInterface, QueryRunner } from "typeorm";
2+
3+
export class Migration1763745645194 implements MigrationInterface {
4+
name = 'Migration1763745645194'
5+
6+
public async up(queryRunner: QueryRunner): Promise<void> {
7+
await queryRunner.query(`ALTER TABLE "vote_reputation_results" DROP CONSTRAINT "FK_292664ed7ffc8782ab097e28ba5"`);
8+
await queryRunner.query(`ALTER TABLE "vote_reputation_results" ALTER COLUMN "groupId" DROP NOT NULL`);
9+
await queryRunner.query(`ALTER TABLE "vote_reputation_results" ADD CONSTRAINT "FK_292664ed7ffc8782ab097e28ba5" FOREIGN KEY ("groupId") REFERENCES "group"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`);
10+
}
11+
12+
public async down(queryRunner: QueryRunner): Promise<void> {
13+
await queryRunner.query(`ALTER TABLE "vote_reputation_results" DROP CONSTRAINT "FK_292664ed7ffc8782ab097e28ba5"`);
14+
await queryRunner.query(`ALTER TABLE "vote_reputation_results" ALTER COLUMN "groupId" SET NOT NULL`);
15+
await queryRunner.query(`ALTER TABLE "vote_reputation_results" ADD CONSTRAINT "FK_292664ed7ffc8782ab097e28ba5" FOREIGN KEY ("groupId") REFERENCES "group"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`);
16+
}
17+
18+
}

platforms/evoting-api/src/services/PollService.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -216,6 +216,11 @@ export class PollService {
216216
throw new Error("eReputation weighted voting cannot be combined with Rank Based Voting (RBV). Please use Simple or PBV mode instead.");
217217
}
218218

219+
// Validate that blind voting (private visibility) and eReputation weighted are not combined
220+
if (pollData.visibility === "private" && votingWeight === "ereputation") {
221+
throw new Error("Blind voting (private visibility) cannot be combined with eReputation weighted voting.");
222+
}
223+
219224
const pollDataForEntity = {
220225
title: pollData.title,
221226
mode: pollData.mode as "normal" | "point" | "rank",

platforms/evoting-api/src/services/VoteService.ts

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -44,20 +44,20 @@ export class VoteService {
4444

4545
/**
4646
* Get reputation score for a user from reputation results using ename
47+
* Returns the actual eReputation score (1-5) to be used as a multiplier
4748
*/
4849
private getReputationScore(ename: string, reputationResults: VoteReputationResult | null): number {
4950
if (!reputationResults || !reputationResults.results) {
50-
return 1.0; // Default weight if no reputation data
51+
return 1.0; // Default score if no reputation data
5152
}
5253

5354
const memberRep = reputationResults.results.find((r: MemberReputation) => r.ename === ename);
5455
if (!memberRep) {
55-
return 1.0; // Default weight if user not found in reputation results
56+
return 1.0; // Default score if user not found in reputation results
5657
}
5758

58-
// Normalize score to a weight (assuming score is 0-5, convert to 0-1 weight)
59-
// You can adjust this formula based on your needs
60-
return Math.max(0.1, memberRep.score / 5.0); // Minimum weight of 0.1, max of 1.0
59+
// Return the actual eReputation score (1-5) to multiply votes/points by
60+
return memberRep.score;
6161
}
6262

6363
/**
@@ -210,6 +210,11 @@ export class VoteService {
210210
const isWeighted = this.isEReputationWeighted(poll);
211211
const reputationResults = isWeighted ? await this.getReputationResults(pollId) : null;
212212

213+
// Validate that reputation results exist if this is a weighted poll
214+
if (isWeighted && !reputationResults) {
215+
throw new Error("eReputation calculation is not yet complete. Results will be available once the calculation finishes.");
216+
}
217+
213218
if (poll.mode === "normal") {
214219
// STEP 1: Calculate results normally (without eReputation weighting)
215220
const optionCounts: Record<string, number> = {};

platforms/evoting-api/src/web3adapter/mappings/vote-reputation-result.mapping.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
"tableName": "vote_reputation_results",
33
"schemaId": "660e8400-e29b-41d4-a716-446655440102",
44
"localToUniversalMap": {
5-
"pollId": "pollId",
5+
"pollId": "polls(pollId),pollId",
66
"groupId": "groupId",
77
"results": "results",
88
"createdAt": "createdAt",

0 commit comments

Comments
 (0)