diff --git a/infrastructure/eid-wallet/src-tauri/gen/android/app/arm64/release/eid-w3ds-v0.2.1.1.apk b/infrastructure/eid-wallet/src-tauri/gen/android/app/arm64/release/eid-w3ds-v0.2.1.1.apk new file mode 100644 index 00000000..a1e76b13 Binary files /dev/null and b/infrastructure/eid-wallet/src-tauri/gen/android/app/arm64/release/eid-w3ds-v0.2.1.1.apk differ diff --git a/infrastructure/eid-wallet/src-tauri/gen/apple/eid-wallet.xcodeproj/project.pbxproj b/infrastructure/eid-wallet/src-tauri/gen/apple/eid-wallet.xcodeproj/project.pbxproj index 13f7e34b..008de526 100644 --- a/infrastructure/eid-wallet/src-tauri/gen/apple/eid-wallet.xcodeproj/project.pbxproj +++ b/infrastructure/eid-wallet/src-tauri/gen/apple/eid-wallet.xcodeproj/project.pbxproj @@ -388,7 +388,7 @@ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CODE_SIGN_ENTITLEMENTS = "eid-wallet_iOS/eid-wallet_iOS.entitlements"; CODE_SIGN_IDENTITY = "iPhone Developer"; - CURRENT_PROJECT_VERSION = 0.2.1.0; + CURRENT_PROJECT_VERSION = 0.2.1.1; DEVELOPMENT_TEAM = M49C8XS835; ENABLE_BITCODE = NO; "EXCLUDED_ARCHS[sdk=iphoneos*]" = x86_64; @@ -436,7 +436,7 @@ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CODE_SIGN_ENTITLEMENTS = "eid-wallet_iOS/eid-wallet_iOS.entitlements"; CODE_SIGN_IDENTITY = "iPhone Developer"; - CURRENT_PROJECT_VERSION = 0.2.1.0; + CURRENT_PROJECT_VERSION = 0.2.1.1; DEVELOPMENT_TEAM = M49C8XS835; ENABLE_BITCODE = NO; "EXCLUDED_ARCHS[sdk=iphoneos*]" = x86_64; diff --git a/infrastructure/eid-wallet/src-tauri/gen/apple/eid-wallet_iOS/Info.plist b/infrastructure/eid-wallet/src-tauri/gen/apple/eid-wallet_iOS/Info.plist index 1f860300..12652f76 100644 --- a/infrastructure/eid-wallet/src-tauri/gen/apple/eid-wallet_iOS/Info.plist +++ b/infrastructure/eid-wallet/src-tauri/gen/apple/eid-wallet_iOS/Info.plist @@ -28,7 +28,7 @@ CFBundleVersion - 0.2.1.0 + 0.2.1.1 LSRequiresIPhoneOS NSAppTransportSecurity diff --git a/infrastructure/eid-wallet/src/routes/(app)/settings/+page.svelte b/infrastructure/eid-wallet/src/routes/(app)/settings/+page.svelte index 188ad556..8dac85cc 100644 --- a/infrastructure/eid-wallet/src/routes/(app)/settings/+page.svelte +++ b/infrastructure/eid-wallet/src/routes/(app)/settings/+page.svelte @@ -126,7 +126,7 @@ on:click={handleVersionTap} disabled={isRetrying} > - Version v0.2.1.0 + Version v0.2.1.1 {#if retryMessage} diff --git a/platforms/eVoting/src/app/(app)/[id]/page.tsx b/platforms/eVoting/src/app/(app)/[id]/page.tsx index 5b476f6d..495941a1 100644 --- a/platforms/eVoting/src/app/(app)/[id]/page.tsx +++ b/platforms/eVoting/src/app/(app)/[id]/page.tsx @@ -132,6 +132,14 @@ export default function Vote({ params }: { params: Promise<{ id: string }> }) { } }, [timeRemaining, selectedPoll]); + // Re-fetch results when poll expires (for all poll types) + useEffect(() => { + if (selectedPoll && timeRemaining === "Voting has ended") { + // Re-fetch fresh results when deadline expires + fetchVoteData(); + } + }, [timeRemaining, selectedPoll]); + // Check if voting is still allowed const isVotingAllowed = selectedPoll && @@ -356,6 +364,26 @@ export default function Vote({ params }: { params: Promise<{ id: string }> }) { Final Results + + {/* Voting Turnout Information */} + {blindVoteResults?.totalEligibleVoters && ( +
+
+
+ + Voting Turnout +
+
+
+ {blindVoteResults.turnout?.toFixed(1) || 0}% +
+
+ {blindVoteResults.totalVotes || 0} of {blindVoteResults.totalEligibleVoters} eligible voters +
+
+
+
+ )}
{blindVoteResults?.optionResults && blindVoteResults.optionResults.length > 0 ? ( blindVoteResults.optionResults.map((result, index) => { @@ -415,6 +443,26 @@ export default function Vote({ params }: { params: Promise<{ id: string }> }) { Final Results + + {/* Voting Turnout Information */} + {resultsData?.totalEligibleVoters && ( +
+
+
+ + Voting Turnout +
+
+
+ {resultsData.turnout?.toFixed(1) || 0}% +
+
+ {resultsData.totalVotes || 0} of {resultsData.totalEligibleVoters} eligible voters +
+
+
+
+ )}
{resultsData?.results && resultsData.results.length > 0 ? ( resultsData.results.map((result, index) => { diff --git a/platforms/eVoting/src/app/(app)/create/page.tsx b/platforms/eVoting/src/app/(app)/create/page.tsx index 6fc13b8c..ea52a915 100644 --- a/platforms/eVoting/src/app/(app)/create/page.tsx +++ b/platforms/eVoting/src/app/(app)/create/page.tsx @@ -34,7 +34,10 @@ const createPollSchema = z.object({ return true; }, "Please select a valid group"), options: z - .array(z.string().min(1, "Option cannot be empty")) + .array(z.string() + .min(1, "Option cannot be empty") + .refine((val) => !val.includes(','), "Commas are not allowed in option text") + ) .min(2, "At least 2 options required"), deadline: z .string() @@ -139,6 +142,16 @@ export default function CreatePoll() { }; const updateOption = (index: number, value: string) => { + // Prevent commas in option text + if (value.includes(',')) { + toast({ + title: "Invalid Option", + description: "Commas are not allowed in option text as they can break the voting system.", + variant: "destructive", + }); + return; + } + const newOptions = [...options]; newOptions[index] = value; setOptions(newOptions); diff --git a/platforms/eVoting/src/lib/pollApi.ts b/platforms/eVoting/src/lib/pollApi.ts index 5b936160..f5dc8e03 100644 --- a/platforms/eVoting/src/lib/pollApi.ts +++ b/platforms/eVoting/src/lib/pollApi.ts @@ -61,6 +61,8 @@ export interface Group { export interface PollResults { poll: Poll; totalVotes: number; + totalEligibleVoters?: number; + turnout?: number; results: { option: string; votes: number; diff --git a/platforms/evoting-api/src/index.ts b/platforms/evoting-api/src/index.ts index 8abb1baa..899a3540 100644 --- a/platforms/evoting-api/src/index.ts +++ b/platforms/evoting-api/src/index.ts @@ -33,6 +33,15 @@ AppDataSource.initialize() } catch (error) { console.error("Failed to initialize SigningController:", error); } + + // Start cron jobs after database is ready + try { + const cronManager = new CronManagerService(); + cronManager.startAllJobs(); + console.log("Cron jobs started successfully"); + } catch (error) { + console.error("Failed to start cron jobs:", error); + } }) .catch((error: unknown) => { console.error("Error during initialization:", error); diff --git a/platforms/evoting-api/src/services/CronManagerService.ts b/platforms/evoting-api/src/services/CronManagerService.ts index fd6ea25f..27d914d7 100644 --- a/platforms/evoting-api/src/services/CronManagerService.ts +++ b/platforms/evoting-api/src/services/CronManagerService.ts @@ -36,11 +36,11 @@ export class CronManagerService { } /** - * Start the deadline check cron job (runs every 10 minutes) + * Start the deadline check cron job (runs every 5 minutes) */ private startDeadlineCheckJob(): void { - // Schedule: every 10 minutes at 0, 10, 20, 30, 40, 50 seconds - this.deadlineCheckJob = cron.schedule('*/10 * * * *', async () => { + // Schedule: every 5 minutes at 0, 5, 10, 15, 20, 25, 30, 35, 40, 45, 50, 55 + this.deadlineCheckJob = cron.schedule('*/5 * * * *', async () => { console.log(`[${new Date().toISOString()}] Running deadline check cron job...`); try { diff --git a/platforms/evoting-api/src/services/DeadlineCheckService.ts b/platforms/evoting-api/src/services/DeadlineCheckService.ts index a3691627..4fa0874b 100644 --- a/platforms/evoting-api/src/services/DeadlineCheckService.ts +++ b/platforms/evoting-api/src/services/DeadlineCheckService.ts @@ -17,12 +17,14 @@ export class DeadlineCheckService { /** * Check for polls with deadlines that have passed and send system messages - * This method is designed to be called by a cron job every 10 minutes + * This method is designed to be called by a cron job every 5 minutes */ async checkExpiredPolls(): Promise { const now = new Date(); try { + console.log(`[${now.toISOString()}] šŸ” Checking for expired polls...`); + // Find all polls with deadlines that have passed and haven't had deadline messages sent yet const expiredPolls = await this.pollRepository .createQueryBuilder("poll") @@ -32,7 +34,12 @@ export class DeadlineCheckService { .andWhere("poll.deadline IS NOT NULL") .getMany(); - console.log(`Found ${expiredPolls.length} expired polls that need deadline messages`); + console.log(`[${now.toISOString()}] šŸ“Š Found ${expiredPolls.length} expired polls that need deadline messages`); + + if (expiredPolls.length === 0) { + console.log(`[${now.toISOString()}] āœ… No expired polls found, nothing to process`); + return; + } for (const poll of expiredPolls) { try { @@ -56,25 +63,15 @@ export class DeadlineCheckService { } try { - // Get the final results for this poll using blind voting tally - let results; - try { - results = await this.voteService.tallyBlindVotes(poll.id); - } catch (error) { - console.log(`Poll ${poll.id} has no blind votes, using basic poll info`); - // If no blind votes, create basic results from poll options - results = { - totalVotes: 0, - optionResults: poll.options.map((option: string, index: number) => ({ - optionId: `option_${index}`, - optionText: option, - voteCount: 0 - })) - }; - } + // Create a simple deadline message similar to poll creation message + const deadlineMessage = this.createSimpleDeadlineMessage(poll); - // Create a comprehensive deadline message with results - const deadlineMessage = this.createDeadlineMessageWithResults(poll, results); + // Log the exact message that's about to be sent + console.log(`[${new Date().toISOString()}] šŸ“¤ About to send deadline message for poll "${poll.title}" (${poll.id}):`); + console.log(`šŸ“ Message content:`); + console.log(`---`); + console.log(deadlineMessage); + console.log(`---`); // Send the system message await this.messageService.createSystemMessage({ @@ -95,27 +92,11 @@ export class DeadlineCheckService { } /** - * Create a comprehensive deadline message with voting results + * Create a simple deadline message similar to poll creation message */ - private createDeadlineMessageWithResults(poll: Poll, results: any): string { - let resultsText = ''; - - if (results && results.optionResults) { - resultsText = '\n\nšŸ“Š **Final Results:**\n'; - results.optionResults.forEach((result: any, index: number) => { - const emoji = index === 0 ? 'šŸ„‡' : index === 1 ? '🄈' : index === 2 ? 'šŸ„‰' : '•'; - resultsText += `${emoji} ${result.optionText}: ${result.voteCount} votes\n`; - }); - } else if (results && results.results) { - // Fallback for old format - resultsText = '\n\nšŸ“Š **Final Results:**\n'; - results.results.forEach((result: any, index: number) => { - const emoji = index === 0 ? 'šŸ„‡' : index === 1 ? '🄈' : index === 2 ? 'šŸ„‰' : '•'; - resultsText += `${emoji} ${result.option}: ${result.votes} votes (${result.percentage.toFixed(1)}%)\n`; - }); - } - - return `šŸ—³ļø **Vote Results Are In!**\n\n"${poll.title}"\n\nVote ID: ${poll.id}\n\nCreated by: ${poll.creator.name}${resultsText}\n\nThis vote has ended. The results are final!`; + private createSimpleDeadlineMessage(poll: Poll): string { + const voteUrl = `${process.env.PUBLIC_EVOTING_URL || 'http://localhost:3000'}/${poll.id}`; + return `eVoting Platform: Vote results are in!\n\n"${poll.title}"\n\nVote ID: ${poll.id}\n\nCreated by: ${poll.creator.name}\n\nView results here`; } /** @@ -128,25 +109,26 @@ export class DeadlineCheckService { }> { const now = new Date(); - const totalExpired = await this.pollRepository.count({ - where: { - deadline: { $lt: now } as any - } - }); + // Use proper TypeORM syntax instead of MongoDB syntax + const totalExpired = await this.pollRepository + .createQueryBuilder("poll") + .where("poll.deadline < :now", { now }) + .andWhere("poll.deadline IS NOT NULL") + .getCount(); - const messagesSent = await this.pollRepository.count({ - where: { - deadline: { $lt: now } as any, - deadlineMessageSent: true - } - }); + const messagesSent = await this.pollRepository + .createQueryBuilder("poll") + .where("poll.deadline < :now", { now }) + .andWhere("poll.deadline IS NOT NULL") + .andWhere("poll.deadlineMessageSent = :sent", { sent: true }) + .getCount(); - const pendingMessages = await this.pollRepository.count({ - where: { - deadline: { $lt: now } as any, - deadlineMessageSent: false - } - }); + const pendingMessages = await this.pollRepository + .createQueryBuilder("poll") + .where("poll.deadline < :now", { now }) + .andWhere("poll.deadline IS NOT NULL") + .andWhere("poll.deadlineMessageSent = :sent", { sent: false }) + .getCount(); return { totalExpired, diff --git a/platforms/evoting-api/src/services/VoteService.ts b/platforms/evoting-api/src/services/VoteService.ts index 448026b0..6bc0b65a 100644 --- a/platforms/evoting-api/src/services/VoteService.ts +++ b/platforms/evoting-api/src/services/VoteService.ts @@ -162,6 +162,12 @@ export class VoteService { const votes = await this.getVotesByPoll(pollId); + // Get group member count for voting turnout calculation + let totalEligibleVoters = 0; + if (poll.groupId) { + totalEligibleVoters = await this.getGroupMemberCount(poll.groupId); + } + if (poll.mode === "normal") { // Count votes for each option const optionCounts: Record = {}; @@ -194,6 +200,8 @@ export class VoteService { return { pollId, totalVotes, + totalEligibleVoters, + turnout: totalEligibleVoters > 0 ? (totalVotes / totalEligibleVoters) * 100 : 0, results }; } else if (poll.mode === "point") { @@ -279,58 +287,22 @@ export class VoteService { return { pollId, totalVotes, + totalEligibleVoters, + turnout: totalEligibleVoters > 0 ? (totalVotes / totalEligibleVoters) * 100 : 0, mode: "point", results }; } else if (poll.mode === "rank") { - // Calculate rank-based voting results using Borda count - const optionScores: Record = {}; - poll.options.forEach((option, index) => { - optionScores[option] = 0; - }); - - votes.forEach(vote => { - if (vote.data.mode === "rank" && Array.isArray(vote.data.data)) { - // Handle the stored format: [{ option: "ranks", points: { "0": 1, "1": 2, "2": 3 } }] - vote.data.data.forEach((item: any) => { - if (item.option === "ranks" && item.points && typeof item.points === 'object') { - // Sort by rank (lowest number = highest rank) - const sortedRankings = Object.entries(item.points).sort((a, b) => (a[1] as number) - (b[1] as number)); - - sortedRankings.forEach((rankData, rankIndex) => { - const optionIndex = parseInt(rankData[0]); - const option = poll.options[optionIndex]; - if (option) { - // Borda count: first place gets n points, second gets n-1, etc. - const points = poll.options.length - rankIndex; - optionScores[option] += points; - } - }); - } - }); - } - }); - - const totalVotes = votes.length; - const results = poll.options.map((option, index) => { - const score = optionScores[option] || 0; - const averageScore = totalVotes > 0 ? score / totalVotes : 0; - return { - option, - totalScore: score, - averageScore: Math.round(averageScore * 100) / 100, - votes: totalVotes // All voters participated in ranking - }; - }); - - // Sort by total score (highest first) - results.sort((a, b) => b.totalScore - a.totalScore); - + // Calculate rank-based voting results using Instant Runoff Voting (IRV) + const irvResult = this.tallyIRV(votes, poll.options); + return { pollId, - totalVotes, + totalVotes: votes.length, + totalEligibleVoters, + turnout: totalEligibleVoters > 0 ? (votes.length / totalEligibleVoters) * 100 : 0, mode: "rank", - results + irvResult }; } @@ -338,6 +310,8 @@ export class VoteService { return { pollId, totalVotes: votes.length, + totalEligibleVoters, + turnout: totalEligibleVoters > 0 ? (votes.length / totalEligibleVoters) * 100 : 0, mode: poll.mode, error: "Unsupported voting mode for results calculation" }; @@ -540,9 +514,17 @@ export class VoteService { const totalVoteCount = Object.values(electionResult.optionResults).reduce((sum, count) => sum + count, 0); + // Get group member count for voting turnout calculation + let totalEligibleVoters = 0; + if (poll.groupId) { + totalEligibleVoters = await this.getGroupMemberCount(poll.groupId); + } + return { pollId, totalVotes: totalVoteCount, + totalEligibleVoters, + turnout: totalEligibleVoters > 0 ? (totalVoteCount / totalEligibleVoters) * 100 : 0, optionResults, verified: electionResult.verified, cryptographicProof: { @@ -675,7 +657,302 @@ export class VoteService { } } - + // ===== IRV TALLYING METHODS ===== + + /** + * Tally Instant Runoff Voting (IRV) for ranked choice votes + */ + private tallyIRV(votes: Vote[], options: string[]): any { + // Parse and validate ballots + const validBallots: Array<{ ballotId: string; ranking: number[] }> = []; + const rejectedReasons: Array<{ ballotId: string; reason: string }> = []; + + for (const vote of votes) { + try { + if (vote.data.mode === "rank" && Array.isArray(vote.data.data)) { + // Handle the stored format: [{ option: "ranks", points: { "0": 1, "1": 2, "2": 3 } }] + const rankData = vote.data.data.find((item: any) => + item.option === "ranks" && item.points && typeof item.points === 'object' + ); + + if (rankData && rankData.points && typeof rankData.points === 'object') { + const ranking = this.parseBallot(rankData.points as Record); + if (ranking) { + validBallots.push({ ballotId: vote.id, ranking }); + } + } + } + } catch (error) { + rejectedReasons.push({ + ballotId: vote.id, + reason: error instanceof Error ? error.message : 'Invalid ballot format' + }); + } + } + + const rejectedBallots = rejectedReasons.length; + + // Handle edge case: no valid ballots + if (validBallots.length === 0) { + return { + winnerIndex: null, + winnerOption: undefined, + rounds: [], + rejectedBallots, + rejectedReasons + }; + } + + // Determine number of candidates from valid ballots + const maxCandidateIndex = Math.max( + ...validBallots.flatMap(b => b.ranking) + ); + const numCandidates = maxCandidateIndex + 1; + + // Handle edge case: only one candidate + if (numCandidates === 1) { + return { + winnerIndex: 0, + winnerOption: options[0], + rounds: [{ + round: 1, + active: [0], + counts: { 0: validBallots.length }, + eliminated: null, + exhausted: 0 + }], + rejectedBallots, + rejectedReasons + }; + } + + // Initialize IRV process + let activeCandidates = Array.from({ length: numCandidates }, (_, i) => i); + let currentBallots: Array<{ ballotId: string; ranking: number[]; currentChoice: number | undefined }> = + validBallots.map(b => ({ ...b, currentChoice: b.ranking[0] })); + const rounds: any[] = []; + let round = 1; + + while (true) { + // Count votes for current round + const counts: Record = {}; + let exhausted = 0; + + for (const ballot of currentBallots) { + if (ballot.currentChoice !== undefined && activeCandidates.includes(ballot.currentChoice)) { + counts[ballot.currentChoice] = (counts[ballot.currentChoice] || 0) + 1; + } else { + exhausted++; + } + } + + const activeVotes = validBallots.length - exhausted; + const majorityThreshold = Math.floor(activeVotes / 2) + 1; + + // Check for winner + let winner: number | null = null; + for (const [candidate, count] of Object.entries(counts)) { + if (count > majorityThreshold) { + winner = parseInt(candidate); + break; + } + } + + // Record this round + rounds.push({ + round, + active: [...activeCandidates], + counts: { ...counts }, + eliminated: null, + exhausted + }); + + // If we have a winner, stop + if (winner !== null) { + return { + winnerIndex: winner, + winnerOption: options[winner], + rounds, + rejectedBallots, + rejectedReasons + }; + } + + // If only one candidate remains, they win + if (activeCandidates.length === 1) { + return { + winnerIndex: activeCandidates[0], + winnerOption: options[activeCandidates[0]], + rounds, + rejectedBallots, + rejectedReasons + }; + } + + // If no active candidates have votes, no winner + if (activeVotes === 0) { + return { + winnerIndex: null, + winnerOption: undefined, + rounds, + rejectedBallots, + rejectedReasons + }; + } + + // Find candidate to eliminate + const candidateToEliminate = this.findCandidateToEliminate(counts, activeCandidates, rounds); + + // Update active candidates + activeCandidates = activeCandidates.filter(c => c !== candidateToEliminate); + + // Update ballots for next round + currentBallots = currentBallots.map(ballot => { + if (ballot.currentChoice === candidateToEliminate) { + // Find next ranked active candidate + const nextChoice = ballot.ranking.find(c => activeCandidates.includes(c)); + return { ...ballot, currentChoice: nextChoice }; + } + return ballot; + }); + + // Mark elimination in the round + rounds[rounds.length - 1].eliminated = candidateToEliminate; + + round++; + } + } + + /** + * Parse ballot points into ranking array + */ + private parseBallot(points: Record): number[] { + if (!points || typeof points !== 'object') { + throw new Error('Invalid points object'); + } + + // Validate and collect rankings + const rankings: Array<{ candidate: number; rank: number }> = []; + const seenRanks = new Set(); + + for (const [candidateStr, rank] of Object.entries(points)) { + const candidate = parseInt(candidateStr); + const rankNum = parseInt(rank.toString()); + + // Validation checks + if (isNaN(candidate) || isNaN(rankNum)) { + throw new Error('Non-integer candidate or rank values'); + } + + if (candidate < 0) { + throw new Error('Negative candidate index'); + } + + if (rankNum < 1) { + throw new Error('Rank must be at least 1'); + } + + if (seenRanks.has(rankNum)) { + throw new Error('Duplicate rank values'); + } + + seenRanks.add(rankNum); + rankings.push({ candidate, rank: rankNum }); + } + + // Sort by rank and return candidate indices + return rankings + .sort((a, b) => a.rank - b.rank) + .map(r => r.candidate); + } + + /** + * Find candidate to eliminate using tie-breaking rules + */ + private findCandidateToEliminate( + counts: Record, + activeCandidates: number[], + rounds: any[] + ): number { + // Find candidates with lowest vote count + let minVotes = Infinity; + let candidatesWithMinVotes: number[] = []; + + for (const candidate of activeCandidates) { + const votes = counts[candidate] || 0; + if (votes < minVotes) { + minVotes = votes; + candidatesWithMinVotes = [candidate]; + } else if (votes === minVotes) { + candidatesWithMinVotes.push(candidate); + } + } + + // If only one candidate with minimum votes, eliminate them + if (candidatesWithMinVotes.length === 1) { + return candidatesWithMinVotes[0]; + } + + // Tie-breaker 1: Fewest first-choice appearances in round 1 + const round1Counts = rounds[0]?.counts || {}; + let minFirstChoice = Infinity; + let candidatesWithMinFirstChoice: number[] = []; + + for (const candidate of candidatesWithMinVotes) { + const firstChoiceVotes = round1Counts[candidate] || 0; + if (firstChoiceVotes < minFirstChoice) { + minFirstChoice = firstChoiceVotes; + candidatesWithMinFirstChoice = [candidate]; + } else if (firstChoiceVotes === minFirstChoice) { + candidatesWithMinFirstChoice.push(candidate); + } + } + + // If only one candidate with minimum first-choice votes, eliminate them + if (candidatesWithMinFirstChoice.length === 1) { + return candidatesWithMinFirstChoice[0]; + } + + // Tie-breaker 2: Lowest index + return Math.min(...candidatesWithMinFirstChoice); + } + + /** + * Get the total number of members in a group for voting turnout calculation + */ + private async getGroupMemberCount(groupId: string): Promise { + try { + const group = await this.groupRepository + .createQueryBuilder('group') + .leftJoinAndSelect('group.members', 'member') + .leftJoinAndSelect('group.admins', 'admin') + .leftJoinAndSelect('group.participants', 'participant') + .where('group.id = :groupId', { groupId }) + .getOne(); + + if (!group) { + return 0; + } + + // Remove duplicates (someone might be both member and admin) + const uniqueMemberIds = new Set(); + + if (group.members) { + group.members.forEach(member => uniqueMemberIds.add(member.id)); + } + if (group.admins) { + group.admins.forEach(admin => uniqueMemberIds.add(admin.id)); + } + if (group.participants) { + group.participants.forEach(participant => uniqueMemberIds.add(participant.id)); + } + + return uniqueMemberIds.size; + } catch (error) { + console.error('Error getting group member count:', error); + return 0; + } + } } export default new VoteService(); \ No newline at end of file diff --git a/platforms/evoting-api/src/web3adapter/watchers/subscriber.ts b/platforms/evoting-api/src/web3adapter/watchers/subscriber.ts index a58a4159..51cb3a70 100644 --- a/platforms/evoting-api/src/web3adapter/watchers/subscriber.ts +++ b/platforms/evoting-api/src/web3adapter/watchers/subscriber.ts @@ -44,11 +44,7 @@ export class PostgresSubscriber implements EntitySubscriberInterface { * Called before entity insertion. */ beforeInsert(event: InsertEvent) { - console.log("šŸ” beforeInsert triggered:", { - tableName: event.metadata.tableName, - target: typeof event.metadata.target === 'function' ? event.metadata.target.name : event.metadata.target, - hasEntity: !!event.entity - }); + } async enrichEntity(entity: any, tableName: string, tableTarget: any) { @@ -246,11 +242,6 @@ export class PostgresSubscriber implements EntitySubscriberInterface { } } else { console.log("āŒ No entity ID found in update event"); - console.log("šŸ” Event details:", { - entity: event.entity, - databaseEntity: event.databaseEntity, - metadata: event.metadata - }); } this.handleChange( @@ -286,7 +277,6 @@ export class PostgresSubscriber implements EntitySubscriberInterface { * Handle entity changes and send to web3adapter */ private async handleChange(entity: any, tableName: string): Promise { - console.log("yoho") // Check if this is a junction table if (tableName === "group_participants") return; @@ -300,7 +290,6 @@ export class PostgresSubscriber implements EntitySubscriberInterface { // Handle regular entity changes const data = this.entityToPlain(entity); - console.log(data, entity) if (!data.id) return; // For Message entities, only process if they are system messages diff --git a/platforms/group-charter-manager-api/src/web3adapter/watchers/subscriber.ts b/platforms/group-charter-manager-api/src/web3adapter/watchers/subscriber.ts index 6aee6700..2e386dda 100644 --- a/platforms/group-charter-manager-api/src/web3adapter/watchers/subscriber.ts +++ b/platforms/group-charter-manager-api/src/web3adapter/watchers/subscriber.ts @@ -242,7 +242,6 @@ export class PostgresSubscriber implements EntitySubscriberInterface { * Handle entity changes and send to web3adapter */ private async handleChange(entity: any, tableName: string): Promise { - console.log("yoho") // Check if this is a junction table if (tableName === "group_participants") return;