======================================================================= Issue #46: feat: Implement meeting voting system (Résolutions & votes) ======================================================================= :State: **CLOSED** :Milestone: Jalon 3: Features Différenciantes 🎯 :Labels: phase:vps,track:software priority:high :Assignees: Unassigned :Created: 2025-10-27 :Updated: 2025-11-17 :URL: `View on GitHub `_ Description =========== .. raw:: html
:: ## Context **Meeting management:** ✅ **80% implemented** - Meeting entity (AGO/AGE types, statuses, agenda) - 8 API endpoints (create, update, complete, cancel, etc.) - Frontend meeting list and management **Missing:** ❌ **Voting/Resolution system** - Votes (Pour/Contre/Abstention) - Résolutions with vote tracking - Quorum calculation - Majority rules (simple, absolute, qualified) - Vote results and minutes generation Belgian copropriété law requires tracking: - Resolutions discussed - Vote results per resolution - Owner attendance and voting power (tantièmes/millièmes) - Quorum validation ## Objective Implement complete voting system for general assemblies (AG). ## Domain Model ### New Entities **1. Resolution** (`backend/src/domain/entities/resolution.rs`) ```rust pub struct Resolution { pub id: Uuid, pub meeting_id: Uuid, pub title: String, pub description: String, pub resolution_type: ResolutionType, // Ordinary, Extraordinary pub majority_required: MajorityType, // Simple, Absolute, Qualified(f64) pub vote_count_pour: i32, pub vote_count_contre: i32, pub vote_count_abstention: i32, pub total_voting_power_pour: f64, // Sum of tantièmes "pour" pub total_voting_power_contre: f64, pub total_voting_power_abstention: f64, pub status: ResolutionStatus, // Pending, Adopted, Rejected pub created_at: DateTime, pub voted_at: Option>, } pub enum ResolutionType { Ordinary, // Majority of votes present Extraordinary, // Qualified majority (e.g., 2/3, 3/4) } pub enum MajorityType { Simple, // 50% + 1 of votes cast Absolute, // 50% + 1 of all votes (including absent) Qualified(f64), // Custom threshold (e.g., 0.67 for 2/3) } pub enum ResolutionStatus { Pending, // Not yet voted Adopted, // Vote passed Rejected, // Vote failed } ``` **2. Vote** (`backend/src/domain/entities/vote.rs`) ```rust pub struct Vote { pub id: Uuid, pub resolution_id: Uuid, pub owner_id: Uuid, pub unit_id: Uuid, pub vote_choice: VoteChoice, pub voting_power: f64, // Tantièmes/millièmes for this unit pub voted_at: DateTime, pub proxy_owner_id: Option, // If voting by proxy } pub enum VoteChoice { Pour, // For Contre, // Against Abstention, // Abstain } ``` ### Business Rules **Quorum validation:** ```rust impl Meeting { pub fn has_quorum(&self, total_units: i32, present_units: i32) -> bool { let attendance_rate = present_units as f64 / total_units as f64; match self.meeting_type { MeetingType::Ordinary => attendance_rate >= 0.5, // 50% MeetingType::Extraordinary => attendance_rate >= 0.67, // 2/3 } } } ``` **Resolution validation:** ```rust impl Resolution { pub fn calculate_result(&self, total_voting_power: f64) -> ResolutionStatus { match self.majority_required { MajorityType::Simple => { // Majority of votes cast if self.vote_count_pour > self.vote_count_contre + self.vote_count_abstention { ResolutionStatus::Adopted } else { ResolutionStatus::Rejected } } MajorityType::Absolute => { // Majority of all possible votes if self.total_voting_power_pour > total_voting_power / 2.0 { ResolutionStatus::Adopted } else { ResolutionStatus::Rejected } } MajorityType::Qualified(threshold) => { // Custom threshold (e.g., 2/3) let pour_ratio = self.total_voting_power_pour / total_voting_power; if pour_ratio >= threshold { ResolutionStatus::Adopted } else { ResolutionStatus::Rejected } } } } } ``` ## Database Schema **New tables:** ```sql -- Resolutions CREATE TABLE resolutions ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), meeting_id UUID NOT NULL REFERENCES meetings(id) ON DELETE CASCADE, title VARCHAR(255) NOT NULL, description TEXT, resolution_type VARCHAR(50) NOT NULL, -- 'Ordinary', 'Extraordinary' majority_required VARCHAR(50) NOT NULL, -- 'Simple', 'Absolute', 'Qualified:0.67' vote_count_pour INT DEFAULT 0, vote_count_contre INT DEFAULT 0, vote_count_abstention INT DEFAULT 0, total_voting_power_pour DECIMAL(10,4) DEFAULT 0, total_voting_power_contre DECIMAL(10,4) DEFAULT 0, total_voting_power_abstention DECIMAL(10,4) DEFAULT 0, status VARCHAR(50) DEFAULT 'Pending', created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, voted_at TIMESTAMP ); -- Votes CREATE TABLE votes ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), resolution_id UUID NOT NULL REFERENCES resolutions(id) ON DELETE CASCADE, owner_id UUID NOT NULL REFERENCES owners(id), unit_id UUID NOT NULL REFERENCES units(id), vote_choice VARCHAR(50) NOT NULL, -- 'Pour', 'Contre', 'Abstention' voting_power DECIMAL(10,4) NOT NULL, proxy_owner_id UUID REFERENCES owners(id), voted_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, UNIQUE(resolution_id, unit_id) -- One vote per unit per resolution ); CREATE INDEX idx_resolutions_meeting ON resolutions(meeting_id); CREATE INDEX idx_votes_resolution ON votes(resolution_id); CREATE INDEX idx_votes_owner ON votes(owner_id); ``` ## API Endpoints (New) ### Resolutions - `POST /api/v1/meetings/:id/resolutions` - Create resolution - `GET /api/v1/meetings/:id/resolutions` - List meeting resolutions - `GET /api/v1/resolutions/:id` - Get resolution details - `PUT /api/v1/resolutions/:id` - Update resolution - `DELETE /api/v1/resolutions/:id` - Delete resolution ### Votes - `POST /api/v1/resolutions/:id/vote` - Cast vote - `GET /api/v1/resolutions/:id/votes` - Get all votes for resolution - `PUT /api/v1/resolutions/:id/close` - Close voting & calculate result - `GET /api/v1/meetings/:id/vote-summary` - Get all resolution results for meeting ## Frontend Components ### 1. ResolutionList.svelte Display resolutions for a meeting with vote counts: ```svelte
{#each resolutions as resolution}

{resolution.title}

{resolution.description}

Pour: {resolution.vote_count_pour} ({resolution.total_voting_power_pour}%) Contre: {resolution.vote_count_contre} Abstention: {resolution.vote_count_abstention}
{#if resolution.status === 'Adopted'} ✓ Adoptée {:else if resolution.status === 'Rejected'} ✗ Rejetée {:else} En attente {/if}
{/each}
``` ### 2. VotingModal.svelte Modal for casting votes: ```svelte

Vote: {resolution.title}

{#each ownerUnits as unit}
Lot {unit.unit_number} ({unit.quota} millièmes)
{/each}
``` ### 3. VoteResults.svelte Display final vote results with charts: ```svelte
{pourPercent.toFixed(1)}%
{contrePercent.toFixed(1)}%
Pour{resolution.vote_count_pour}{resolution.total_voting_power_pour} millièmes
Contre{resolution.vote_count_contre}{resolution.total_voting_power_contre} millièmes
Abstention{resolution.vote_count_abstention}{resolution.total_voting_power_abstention} millièmes
``` ## PDF Generation Integration Update meeting minutes PDF to include vote results: ```rust // In pcn_exporter.rs or new meeting_minutes_exporter.rs pub fn generate_meeting_minutes_pdf(meeting: &Meeting, resolutions: Vec) -> Vec { // Add resolution results to PDF for resolution in resolutions { doc.add_page(...); doc.add_text(format!("Résolution: {}", resolution.title)); doc.add_text(format!("Pour: {} votes ({} millièmes)", resolution.vote_count_pour, resolution.total_voting_power_pour)); doc.add_text(format!("Résultat: {}", resolution.status)); } } ``` ## Testing - [ ] Create resolution - [ ] Cast vote (Pour/Contre/Abstention) - [ ] Calculate quorum - [ ] Calculate vote results (Simple/Absolute/Qualified majority) - [ ] Close voting and finalize status - [ ] Generate PDF with vote results - [ ] Proxy voting - [ ] Prevent duplicate votes per unit ## Acceptance Criteria - [ ] Resolution and Vote entities implemented - [ ] Database migrations complete - [ ] 8 new API endpoints functional - [ ] Quorum calculation correct - [ ] Majority calculation (3 types) correct - [ ] Frontend voting UI complete - [ ] Vote results display complete - [ ] PDF generation includes vote results - [ ] Tests passing - [ ] Documentation updated ## Effort Estimate **Medium** (2-3 days) - Day 1: Domain entities + database + repositories - Day 2: Use cases + API endpoints + business logic - Day 3: Frontend components + PDF integration + testing ## Related - Enhances: Meeting management (Issue in roadmap) - Supports: PDF generation (procès-verbaux) - Complies with: Belgian copropriété law ## References - Belgian copropriété law: https://www.notaire.be/ - Voting systems: https://fr.wikipedia.org/wiki/Syst%C3%A8me_de_vote .. raw:: html