Skip to content
This repository was archived by the owner on May 29, 2026. It is now read-only.
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
bfb7d4a
feat: add motion-and-voting lifecycle services, controllers, views, a…
Apr 13, 2026
66a9b9a
feat(quality): fix PHPCS and ESLint violations in new files (#18)
Apr 13, 2026
ccf20ab
feat(spec): mark tasks completed and update design status to pr-creat…
Apr 13, 2026
df97694
quality fix: phpunit + stylelint — named param & CSS quotes (#18)
Apr 13, 2026
43edbca
fix iteration 1: address all CRITICAL and WARNING review findings (#18)
Apr 13, 2026
7dbda12
fix iteration 2: address all CRITICAL and WARNING review findings (#18)
Apr 13, 2026
953b95c
fix iteration 2: address all CRITICAL and WARNING review findings (#18)
Apr 13, 2026
81994de
fix iteration 2: repair broken VotingServiceTest and add revokeProxy …
Apr 13, 2026
4d1ab22
fix iteration 3: address all CRITICAL and WARNING review findings (#18)
Apr 13, 2026
a6cd80c
fix iteration 3: address all CRITICAL and WARNING review findings (#18)
Apr 13, 2026
f6ccc10
Merge remote-tracking branch 'origin/development' into feature/18/p2-…
Apr 13, 2026
c8837d3
fix iteration 4: fix all CRITICAL and WARNING review findings (#18)
Apr 13, 2026
9a3beae
chore: review round 8 results (#24)
rubenvdlinde Apr 13, 2026
9c90e99
chore: review round 9 results (#23)
rubenvdlinde Apr 13, 2026
b0c5b1b
chore: review round 7 results (#20)
rubenvdlinde Apr 13, 2026
4b3a964
fix iteration 4: address all CRITICAL and WARNING review findings (#18)
Apr 13, 2026
dadab21
fix iteration 5: merge development into feature branch (#18)
Apr 13, 2026
45c6563
fix iteration 5: address all critical and warning review findings (#18)
Apr 13, 2026
1cae220
Merge remote-tracking branch 'origin/development' into feature/18/p2-…
Apr 14, 2026
7fc054d
fix iteration 6: add @nextcloud/auth to package.json dependencies (#18)
Apr 14, 2026
ede0768
fix iteration 6: repair testCastVoteProxyOnePerRound fixture (#18)
Apr 14, 2026
5a80d4c
Merge remote-tracking branch 'origin/development' into feature/18/p2-…
Apr 14, 2026
0043f9d
fix iteration 7: replace raw exception messages in grantProxy/revokeP…
Apr 14, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions appinfo/info.xml
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ Vrij en open source onder de EUPL-1.2-licentie.
**Ondersteuning:** Voor ondersteuning, neem contact op via support@conduction.nl.
]]></description>
<version>0.1.0</version>
<licence>agpl</licence>
<licence>eupl</licence>
<author mail="info@conduction.nl" homepage="https://www.conduction.nl/">Conduction</author>
<namespace>Decidesk</namespace>
<documentation>
Expand Down Expand Up @@ -71,8 +71,9 @@ Vrij en open source onder de EUPL-1.2-licentie.
</post-migration>
</repair-steps>

<settings>
<settings>
<admin>OCA\Decidesk\Settings\AdminSettings</admin>
<admin-section>OCA\Decidesk\Sections\SettingsSection</admin-section>
</settings>

</info>
16 changes: 16 additions & 0 deletions appinfo/routes.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,22 @@
// Health check endpoint.
['name' => 'health#index', 'url' => '/api/health', 'verb' => 'GET'],

// Motion lifecycle routes.
['name' => 'motion#transition', 'url' => '/api/motions/{id}/transition', 'verb' => 'POST'],
['name' => 'motion#coSignRequest', 'url' => '/api/motions/{id}/co-sign-request', 'verb' => 'POST'],
['name' => 'motion#coSignConfirm', 'url' => '/api/motions/{id}/co-sign-confirm', 'verb' => 'POST'],
['name' => 'motion#budgetImpact', 'url' => '/api/motions/{id}/budget-impact', 'verb' => 'POST'],
['name' => 'motion#amendmentTransition', 'url' => '/api/amendments/{id}/transition', 'verb' => 'POST'],

// Voting round routes.
['name' => 'voting#open', 'url' => '/api/voting-rounds', 'verb' => 'POST'],
['name' => 'voting#cast', 'url' => '/api/voting-rounds/{id}/cast', 'verb' => 'POST'],
['name' => 'voting#close', 'url' => '/api/voting-rounds/{id}/close', 'verb' => 'POST'],
['name' => 'voting#publish', 'url' => '/api/voting-rounds/{id}/publish', 'verb' => 'POST'],
['name' => 'voting#handsCount', 'url' => '/api/voting-rounds/{id}/hands-count', 'verb' => 'POST'],
['name' => 'voting#grantProxy', 'url' => '/api/voting-rounds/{id}/proxy', 'verb' => 'POST'],
['name' => 'voting#revokeProxy', 'url' => '/api/voting-rounds/{id}/proxy', 'verb' => 'DELETE'],

// SPA catch-all — same controller as the index route; must use a distinct route name
// (duplicate names replace the earlier route in Symfony, which breaks GET /).
['name' => 'dashboard#catchAll', 'url' => '/{path}', 'verb' => 'GET', 'requirements' => ['path' => '.+'], 'defaults' => ['path' => '']],
Expand Down
81 changes: 80 additions & 1 deletion l10n/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,86 @@
"Settings saved successfully": "Settings saved successfully",
"Saving...": "Saving...",
"This app needs OpenRegister to store and manage data. Please install OpenRegister from the app store to get started.": "This app needs OpenRegister to store and manage data. Please install OpenRegister from the app store to get started.",
"User settings will appear here in a future update.": "User settings will appear here in a future update."
"User settings will appear here in a future update.": "User settings will appear here in a future update.",
"Motions": "Motions",
"Motion": "Motion",
"Motions overview": "Motions overview",
"All statuses": "All statuses",
"All types": "All types",
"Submitted": "Submitted",
"Debating": "Debating",
"Voting": "Voting",
"Adopted": "Adopted",
"Rejected": "Rejected",
"Withdrawn": "Withdrawn",
"Order motion": "Order motion",
"Procedural": "Procedural",
"Title": "Title",
"Type": "Type",
"Proposer": "Proposer",
"Status": "Status",
"No motions found": "No motions found",
"Motion text": "Motion text",
"Co-signers": "Co-signers",
"No co-signers yet": "No co-signers yet",
"Participant name": "Participant name",
"Co-signer name": "Co-signer name",
"Add co-signer": "Add co-signer",
"Budget impact": "Budget impact",
"Budget line": "Budget line",
"Amount delta": "Amount delta",
"Rationale": "Rationale",
"Save budget impact": "Save budget impact",
"Amendments": "Amendments",
"Amendment": "Amendment",
"{count} amendments": "{count} amendments",
"Submit amendment": "Submit amendment",
"No amendments submitted": "No amendments submitted",
"Amendment text": "Amendment text",
"Possible conflict with another amendment — consult the clerk": "Possible conflict with another amendment — consult the clerk",
"Motion lifecycle": "Motion lifecycle",
"Amendment lifecycle": "Amendment lifecycle",
"Result": "Result",
"Open debate": "Open debate",
"Open voting round": "Open voting round",
"Withdraw motion": "Withdraw motion",
"Voting method": "Voting method",
"For / Against / Abstain": "For / Against / Abstain",
"Show of hands": "Show of hands",
"Ranked choice": "Ranked choice",
"Weighted": "Weighted",
"Secret ballot": "Secret ballot",
"Deadline (optional)": "Deadline (optional)",
"Voting deadline": "Voting deadline",
"Failed to open voting round": "Failed to open voting round",
"Method": "Method",
"Cast your vote": "Cast your vote",
"Vote for": "Vote for",
"Vote against": "Vote against",
"For": "For",
"Against": "Against",
"Abstain": "Abstain",
"Vote registered": "Vote registered",
"Votes cast": "Votes cast",
"Show of hands count": "Show of hands count",
"Votes for": "Votes for",
"Votes against": "Votes against",
"Abstentions": "Abstentions",
"Save result": "Save result",
"Grant proxy": "Grant proxy",
"Delegate participant ID": "Delegate participant ID",
"Proxy delegate": "Proxy delegate",
"Confirm proxy": "Confirm proxy",
"Revoke proxy": "Revoke proxy",
"Close voting round": "Close voting round",
"Voting result": "Voting result",
"Tied": "Tied",
"Invalid": "Invalid",
"Publish to ORI": "Publish to ORI",
"You are voting on behalf of: {name}": "You are voting on behalf of: {name}",
"ORI endpoint": "ORI endpoint",
"Email voting": "Email voting",
"Enable email voting": "Enable email voting"
},
"plurals": ""
}
81 changes: 80 additions & 1 deletion l10n/nl.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,86 @@
"Settings saved successfully": "Instellingen succesvol opgeslagen",
"Saving...": "Opslaan...",
"This app needs OpenRegister to store and manage data. Please install OpenRegister from the app store to get started.": "Deze app heeft OpenRegister nodig om gegevens op te slaan en te beheren. Installeer OpenRegister via de app store om te beginnen.",
"User settings will appear here in a future update.": "Gebruikersinstellingen verschijnen hier in een toekomstige update."
"User settings will appear here in a future update.": "Gebruikersinstellingen verschijnen hier in een toekomstige update.",
"Motions": "Moties",
"Motion": "Motie",
"Motions overview": "Moties overzicht",
"All statuses": "Alle statussen",
"All types": "Alle typen",
"Submitted": "Ingediend",
"Debating": "Debat",
"Voting": "Stemming",
"Adopted": "Aangenomen",
"Rejected": "Verworpen",
"Withdrawn": "Ingetrokken",
"Order motion": "Motie vreemd aan de orde",
"Procedural": "Procedureel",
"Title": "Titel",
"Type": "Type",
"Proposer": "Indiener",
"Status": "Status",
"No motions found": "Geen moties gevonden",
"Motion text": "Motietekst",
"Co-signers": "Medeondertekenaars",
"No co-signers yet": "Nog geen medeondertekenaars",
"Participant name": "Naam deelnemer",
"Co-signer name": "Naam medeondertekenaar",
"Add co-signer": "Medeondertekenaar toevoegen",
"Budget impact": "Begrotingsimpact",
"Budget line": "Begrotingspost",
"Amount delta": "Bedragwijziging",
"Rationale": "Onderbouwing",
"Save budget impact": "Begrotingsimpact opslaan",
"Amendments": "Amendementen",
"Amendment": "Amendement",
"{count} amendments": "{count} amendementen",
"Submit amendment": "Amendement indienen",
"No amendments submitted": "Geen amendementen ingediend",
"Amendment text": "Amendementtekst",
"Possible conflict with another amendment — consult the clerk": "Mogelijk conflict met ander amendement — raadpleeg de griffier",
"Motion lifecycle": "Motie levenscyclus",
"Amendment lifecycle": "Amendement levenscyclus",
"Result": "Resultaat",
"Open debate": "Debat openen",
"Open voting round": "Stemronde openen",
"Withdraw motion": "Motie intrekken",
"Voting method": "Stemmethode",
"For / Against / Abstain": "Voor / Tegen / Onthouding",
"Show of hands": "Handopsteking",
"Ranked choice": "Voorkeurstemming",
"Weighted": "Gewogen",
"Secret ballot": "Geheime stemming",
"Deadline (optional)": "Deadline (optioneel)",
"Voting deadline": "Stemdeadline",
"Failed to open voting round": "Kan stemronde niet openen",
"Method": "Methode",
"Cast your vote": "Breng uw stem uit",
"Vote for": "Stem voor",
"Vote against": "Stem tegen",
"For": "Voor",
"Against": "Tegen",
"Abstain": "Onthouding",
"Vote registered": "Stem geregistreerd",
"Votes cast": "Stemmen uitgebracht",
"Show of hands count": "Telling handopsteking",
"Votes for": "Stemmen voor",
"Votes against": "Stemmen tegen",
"Abstentions": "Onthoudingen",
"Save result": "Resultaat opslaan",
"Grant proxy": "Volmacht verlenen",
"Delegate participant ID": "Deelnemer-ID gemachtigde",
"Proxy delegate": "Volmachtgemachtigde",
"Confirm proxy": "Volmacht bevestigen",
"Revoke proxy": "Volmacht intrekken",
"Close voting round": "Stemronde sluiten",
"Voting result": "Stemresultaat",
"Tied": "Gelijk",
"Invalid": "Ongeldig",
"Publish to ORI": "Publiceren naar ORI",
"You are voting on behalf of: {name}": "U stemt namens: {name}",
"ORI endpoint": "ORI-eindpunt",
"Email voting": "E-mail stemmen",
"Enable email voting": "E-mail stemmen inschakelen"
},
"plurals": ""
}
124 changes: 124 additions & 0 deletions lib/BackgroundJob/MailReplyHandler.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
<?php

/**
* SPDX-License-Identifier: EUPL-1.2
* Copyright (C) 2026 Conduction B.V.
*
* Decidesk Mail Reply Handler
*
* Background job that polls for email vote replies and parses vote keywords.
*
* Runs every 5 minutes to check a configured mailbox for incoming vote replies.
* Recognised keywords are the Dutch voting terms: Voor (for), Tegen (against),
* and Onthouding (abstain).
*
* @category BackgroundJob
* @package OCA\Decidesk\BackgroundJob
*
* @author Conduction Development Team <dev@conductio.nl>
* @copyright 2026 Conduction B.V.
* @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12
*
* @version GIT: <git-id>
*
* @link https://conduction.nl
*/

declare(strict_types=1);

namespace OCA\Decidesk\BackgroundJob;

use OCA\Decidesk\AppInfo\Application;
use OCP\IAppConfig;
use Psr\Container\ContainerInterface;
use Psr\Log\LoggerInterface;

/**
* Background job that polls for email vote replies and parses vote keywords.
*
* @spec openspec/changes/p2-motion-and-voting/tasks.md#task-3
*/
class MailReplyHandler extends \OCP\BackgroundJob\TimedJob
{

/**
* The service container for resolving dependencies.
* Stored for use when IMAP polling is fully implemented (deferred to p3).
*
* @var ContainerInterface
*/
private ContainerInterface $container;

/**
* The logger instance.
*
* @var LoggerInterface
*/
private LoggerInterface $logger;

/**
* The Nextcloud app configuration service.
*
* @var IAppConfig
*/
private IAppConfig $appConfig;

/**
* Constructor.
*
* @param \OCP\AppFramework\Utility\ITimeFactory $time The time factory for scheduling.
* @param ContainerInterface $container The service container for resolving dependencies.
* @param LoggerInterface $logger The logger instance.
* @param IAppConfig $appConfig The Nextcloud app configuration service.
*/
public function __construct(
\OCP\AppFramework\Utility\ITimeFactory $time,
ContainerInterface $container,
LoggerInterface $logger,
IAppConfig $appConfig,
) {
parent::__construct(time: $time);
$this->setInterval(seconds: 300);

$this->container = $container;
$this->logger = $logger;
$this->appConfig = $appConfig;
}//end __construct()

/**
* Execute the background job.
*
* Checks whether email voting is enabled in the app configuration. When
* enabled, polls the configured mailbox for vote reply messages and parses
* recognised vote keywords (Voor, Tegen, Onthouding) to record votes.
*
* @param mixed $argument The job argument (unused).
*
* @return void
*/
protected function run($argument): void
{
$emailVotingEnabled = $this->appConfig->getValueString(
Application::APP_ID,
'email_voting_enabled',
'false',
);

if ($emailVotingEnabled !== 'true') {
return;
}//end if

$this->logger->info('MailReplyHandler: checking for email vote replies');

// In production, this would:
// 1. Connect to the configured IMAP mailbox
// 2. Fetch unread messages matching the vote reply subject pattern
// 3. Parse the message body for vote keywords:
// - "Voor" → vote in favour
// - "Tegen" → vote against
// - "Onthouding" → abstain
// 4. Match the sender to a council member
// 5. Record the vote on the corresponding VotingRound via ObjectService
// 6. Mark the email as processed.
}//end run()
}//end class
Loading
Loading