feat(portal): citizen create actions — reactions + budget proposals#354
Merged
rubenvdlinde merged 1 commit intoJul 23, 2026
Merged
Conversation
…portal-citizen-create-actions) Extends the citizen manifest's read-only ADR-046 contribution with two type:create actions: createReaction (consultation-reaction) and createBudgetProposal (budget-proposal). Scope is stamped server-side via portaliq's scopeField mechanism (submitterId/submitter), never client-writable; intake state (moderationStatus/status) is stamped via defaults, also never client-writable. minTrust stays low (account-less participation). Portaliq's shared create receiver does not enforce the declared parentConstraint, so a new PortalCreateOpenParentGuardListener enforces it server-side on OpenRegister's ObjectCreatingEvent (fail-closed reject before persist): createReaction requires PublicConsultation.status == open; createBudgetProposal requires ParticipatoryBudget.status == submission. Unit tests cover the manifest shape, the write-IDOR/lifecycle invariant, a register-drift pin against decidesk_register.json, and the guard listener's fail-closed matrix (both write-path shapes, missing parent, unrelated schema, infrastructure failure).
rubenvdlinde
requested review from
Rem-Dam,
WilcoLouwerse,
bbrands02 and
rjzondervan
as code owners
July 23, 2026 18:33
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
portal-citizen-create-actionsOpenSpec change: thecitizenADR-046 manifest gains twotype: createactions —createReaction(consultation-reaction) andcreateBudgetProposal(budget-proposal) — closing the write-IDOR/read-only gap left byportal-contribution.scopeFieldmechanism (submitterId/submitter), never client-writable; the intake state (moderationStatus: 'pending'/status: 'submitted') is stamped viadefaults, also never client-writable.minTruststayslow(account-less participation).ContributionController::create()+PortalObjectWriter) usesscopeField+defaults, not the brief'ssetshorthand, and does not enforce a declaredparentConstraint. Closed with a newPortalCreateOpenParentGuardListeneron OpenRegister'sObjectCreatingEvent— fail-closed reject (viastopPropagation()→HookStoppedException, before the row is persisted) when the parentPublicConsultationisn'tstatus: open(createReaction) or the parentParticipatoryBudgetisn'tstatus: submission(createBudgetProposal).openspec/specs/portal-citizen-create-actions/spec.md;openspec validate --specs --strictgreen (97/97).Test plan
tests/Unit/Portal/PortalContributionProviderTest.php(extended, 21 tests total): manifest shape, exact whitelists/scopeField/defaults/parentConstraint, write-IDOR/staff-field invariant, register-drift pin againstdecidesk_register.json.tests/Unit/Listener/PortalCreateOpenParentGuardListenerTest.php(new, 9 tests): open/closed consultation, submission/draft budget round, both write-path shapes (scalar field vsrelationsarray), missing parent, unrelated schema, non-ObjectCreatingEvent, infrastructure failure fails closed (deliberate contrast withSubmissionDeadlineListener's fail-soft business-rule posture — this is a security invariant).nextcloud:34.0.0-apachecontainer: 790/790 pass except 1 pre-existing, unrelated failure (RegisterJsonTest::testGovernanceBodySchema, confirmed present on unmodifiedorigin/developmentbefore this PR).lib/scope): clean on all touched files (also fixed one pre-existing full-stop nit inApplication.phpencountered while editing it).citizenContribution()/handle()/resolveSchemaSlug()to stay under the method-length/cyclomatic-complexity thresholds).Application.php; project has ~66 pre-existing errors overall, masked today bycomposer test:all's|| echo skipfallback — out of scope for this PR).--scope-to-diff): 38/39 green;spec-anchor-existencefails on 52 pre-existing, unrelated broken@specanchors already inApplication.phpfrom older archived changes (none touch this PR's new code or new@spectags) — filed as a known systemic issue, not addressed here (out of scope).@e2e excludescenarios.