Skip to content

Ideation & Challenges Module

Audience: maintainers and contributors working on community idea management, challenge lifecycles, voting, outcomes, or campaign organisation.


What it does

The Ideation & Challenges module lets tenants run structured idea-collection campaigns:

  • Admins create challenges — community problem statements with a lifecycle, deadlines, optional categories, tags, and a prize description.
  • Members submit ideas in response to a challenge, vote on each other's ideas, comment, and save drafts.
  • Admins progress challenges through a controlled lifecycle, shortlist and select winning ideas, and record outcomes (implementation status and impact).
  • Campaigns group related challenges into a themed collection.
  • Templates allow admins to pre-configure recurring challenge patterns.
  • Winning ideas can be converted into groups (implementation teams) with a dedicated chatroom, task board, and document store.

Not to be confused with Gamification Challenges. The challenges table and ChallengeService power time-bound XP/badge rewards on the gamification track (GET /v2/gamification/challenges). Ideation challenges use the ideation_challenges table and IdeationChallengeService. The two systems are fully independent — they share only the word "challenge".


Feature gate

Feature flag: ideation_challenges (default: ON).

  • PHP: TenantContext::hasFeature('ideation_challenges') — checked in IdeationChallengesController::ensureFeature() before every action. Returns HTTP 403 when the feature is disabled.
  • React: all ideation routes are wrapped in <FeatureGate feature="ideation_challenges">. When disabled, the browse page shows a coming-soon placeholder; deeper routes redirect to /.
  • Accessible (GOV.UK) frontend: gated in AlphaController using the same TenantContext::hasFeature() check (see app/Http/Controllers/GovukAlpha/Concerns/IdeationParity.php).
  • Default value declared in app/Services/TenantFeatureConfig.php.

Challenge lifecycle

Status transitions are validated server-side. Only admins can advance status.

draft → open → voting → evaluating → closed → archived
                └──────── evaluating → closed ──────────────┘
                          closed → open   (re-open)

Allowed transitions are enforced by IdeationChallengeService::updateChallengeStatus(). Attempting an invalid transition returns HTTP 409.

Idea statuses (admin-controlled via updateIdeaStatus()): submitted → shortlisted → winner → withdrawn.


Database tables

All ideation tables are prefixed with challenge_ or ideation_; they carry no cross-tenant data.

Table Purpose
ideation_challenges Root challenge records; holds tenant_id, lifecycle status, deadlines, tags (JSON), ideas_count, favorites_count, views_count, is_featured
challenge_ideas Idea submissions; challenge_id FK; status (draft, submitted, shortlisted, winner, withdrawn); votes_count, comments_count
challenge_idea_votes One row per (idea_id, user_id) pair — toggled atomically
challenge_idea_comments Comments on ideas
challenge_favorites User bookmarks on challenges
challenge_categories Tenant-scoped taxonomy for classifying challenges; has slug, icon, color, sort_order
challenge_tags Reusable tags; linked via challenge_tag_links
challenge_tag_links Many-to-many join between challenge_tags and ideation_challenges
challenge_templates Admin-created templates with pre-filled fields; default_tags + evaluation_criteria stored as JSON
challenge_outcomes One outcome per challenge; tracks winning_idea_id, implementation status (not_started, in_progress, implemented, abandoned), and impact_description
idea_media Files or URLs attached to ideas
idea_team_links Links a converted idea to its implementation group
campaigns Thematic collections of challenges
campaign_challenges Many-to-many join with sort_order
group_chatrooms Chatrooms attached to implementation groups
group_chatroom_messages Messages inside chatrooms; supports pinning
team_tasks Kanban tasks inside implementation groups
team_documents File uploads inside implementation groups

Tenant scoping: ideation_challenges.tenant_id is the root anchor. Ideas, votes, and comments are scoped transitively by joining through their parent challenge. Categories, tags, templates, and outcomes carry their own tenant_id via the HasTenantScope trait on the relevant Eloquent models.


Backend

Services

Service Responsibility
app/Services/IdeationChallengeService.php Core CRUD for challenges, ideas, votes, comments, favorites, and draft management; sends email/push notifications to recipients in their preferred language via LocaleContext::withLocale()
app/Services/ChallengeCategoryService.php Per-tenant category CRUD (admin only)
app/Services/ChallengeTagService.php Tag CRUD (admin-create/delete; any authenticated user can list)
app/Services/ChallengeTemplateService.php Template CRUD; getTemplateData() returns pre-filled form defaults for the challenge-create flow
app/Services/ChallengeOutcomeService.php Upsert/read outcome record; getDashboard() returns aggregate stats across all closed challenges
app/Services/CampaignService.php Campaign CRUD and challenge-linking
app/Services/IdeaTeamConversionService.php Promotes a winning idea to an implementation group; records the link in idea_team_links
app/Services/IdeaMediaService.php Media attachments on ideas
app/Services/GroupChatroomService.php Chatrooms and messages inside implementation groups
app/Services/TeamTaskService.php Task board for implementation groups
app/Services/TeamDocumentService.php Document uploads for implementation groups

Controllers

Controller Path prefix Notes
app/Http/Controllers/Api/IdeationChallengesController.php /api/v2/ All public and member-facing endpoints; also hosts group chatroom, task, and document endpoints that belong to implementation teams
app/Http/Controllers/Api/AdminIdeationController.php /api/v2/admin/ideation Moderation view: list all challenges (offset-paginated), show, delete, advance status; requires admin middleware

Key API endpoints

All endpoints require auth:sanctum unless noted. See routes/api.php for the full list.

Challenges

Method Path Auth Notes
GET /v2/ideation-challenges optional Cursor-paginated; ?status=, ?category_id=, ?search=, ?cursor=, ?per_page= (1–100)
POST /v2/ideation-challenges required Creates challenge; fires feed-activity record; rate-limited 10 req/min
GET /v2/ideation-challenges/{id} optional Detail with ideas_count and is_favorited
PUT /v2/ideation-challenges/{id} admin Update challenge fields
DELETE /v2/ideation-challenges/{id} admin Hard delete
PUT /v2/ideation-challenges/{id}/status admin Status transition (validates allowed paths)
POST /v2/ideation-challenges/{id}/favorite required Toggle bookmark
POST /v2/ideation-challenges/{id}/duplicate admin Copies challenge as draft with [Copy] prefix

Ideas

Method Path Notes
GET /v2/ideation-challenges/{id}/ideas ?sort=votes (default) or newest; cursor-paginated
POST /v2/ideation-challenges/{id}/ideas Submit idea; notifies challenge creator
GET /v2/ideation-challenges/{id}/ideas/drafts User's own drafts for a challenge
GET /v2/ideation-ideas/{id} Single idea; includes has_voted when authenticated
PUT /v2/ideation-ideas/{id} Edit own idea while challenge is open
PUT /v2/ideation-ideas/{id}/draft Save/publish a draft (?publish=true transitions draft → submitted)
DELETE /v2/ideation-ideas/{id} Owner or admin; decrements counter atomically
POST /v2/ideation-ideas/{id}/vote Toggle vote; blocked if idea is draft/withdrawn or challenge is not open/voting; users cannot vote on their own ideas
PUT /v2/ideation-ideas/{id}/status Admin only; submitted → shortlisted → winner → withdrawn; notifies idea author
POST /v2/ideation-ideas/{id}/convert-to-group Create implementation group from idea

Comments

Method Path
GET /v2/ideation-ideas/{id}/comments
POST /v2/ideation-ideas/{id}/comments
DELETE /v2/ideation-comments/{id}

Taxonomy and templates

Method Path Notes
GET /v2/ideation-categories Public list
POST/PUT/DELETE /v2/ideation-categories/{id} Admin only
GET /v2/ideation-tags ?type= filter
GET /v2/ideation-tags/popular Ranked by usage count
POST/DELETE /v2/ideation-tags/{id} Admin only
GET /v2/ideation-templates Admin and member
GET /v2/ideation-templates/{id}/data Pre-filled form defaults
POST/PUT/DELETE /v2/ideation-templates/{id} Admin only

Outcomes and campaigns

Method Path
GET/PUT /v2/ideation-challenges/{id}/outcome
GET /v2/ideation-outcomes/dashboard
GET/POST/PUT/DELETE /v2/ideation-campaigns, /v2/ideation-campaigns/{id}
POST/DELETE /v2/ideation-campaigns/{id}/challenges

Admin moderation

Method Path
GET /v2/admin/ideation — offset-paginated list; ?status=, ?search=, ?page=, ?limit= (max 200)
GET /v2/admin/ideation/{id}
DELETE /v2/admin/ideation/{id}
POST /v2/admin/ideation/{id}/status

Frontend entry points

React frontend (react-frontend/src/):

File Route
pages/ideation/IdeationPage.tsx /{tenant}/ideation
pages/ideation/ChallengeDetailPage.tsx /{tenant}/ideation/:id
pages/ideation/IdeaDetailPage.tsx /{tenant}/ideation/:challengeId/ideas/:id
pages/ideation/CreateChallengePage.tsx /{tenant}/ideation/create (authenticated)
pages/ideation/CampaignsPage.tsx /{tenant}/ideation/campaigns
pages/ideation/CampaignDetailPage.tsx /{tenant}/ideation/campaigns/:id
pages/ideation/OutcomesDashboardPage.tsx /{tenant}/ideation/outcomes
admin/modules/ideation/IdeationAdmin.tsx /admin/ideation
components/ideation/ TeamChatrooms, TeamDocuments, TeamTasks — rendered inside the group/team detail view

Accessible (GOV.UK) frontend (accessible-frontend/views/):

ideation.blade.php, ideation-idea.blade.php, ideation-detail.blade.php, ideation-challenge-form.blade.php, ideation-drafts.blade.php, ideation-manage.blade.php, ideation-outcome-form.blade.php, ideation-outcomes.blade.php, ideation-campaigns.blade.php, ideation-campaign-detail.blade.php, ideation-tags.blade.php.

Controller trait: app/Http/Controllers/GovukAlpha/Concerns/IdeationParity.php. Lang file: lang/en/govuk_alpha_ideation.php (plus 10 locale variants).


Security and privacy invariants

  • Every query on ideation_challenges includes WHERE tenant_id = ? (checked in IdeationChallengeService via TenantContext::getId()). Ideas, votes, and comments are scoped transitively by joining back to the parent challenge.
  • Voting: users cannot vote on their own ideas; votes are rejected if the challenge is not in open or voting status; a duplicate vote toggles the existing vote off (idempotent toggle).
  • Challenge update and delete are admin-only. Idea edit is owner-only and blocked once the challenge leaves open status.
  • Draft ideas are owner-private: getUserDrafts() always filters by both challenge_id and user_id.
  • Outcome upsert (including selecting a winning idea) is admin-only. The winning idea's membership in the challenge is validated before the FK is written.
  • Duplicate operation creates a draft with zeroed counters; deadlines are intentionally cleared.
  • All notification emails are rendered inside LocaleContext::withLocale($recipient, ...) so they go out in the recipient's preferred_language, not the caller's locale.

Notifications

IdeationChallengeService fires in-app notifications and push (via NotificationDispatcher::fanOutPush) and templated HTML email (via EmailDispatchService::sendRaw) for:

Trigger Recipient
New idea submitted to a challenge Challenge creator
Vote cast on an idea Idea author (not on unvote)
Comment added to an idea Idea author
Idea status changed by admin Idea author

Email templates use lang/en/emails_ideation.json (plus 10 locale variants).


Prerender invalidation

app/Observers/IdeationChallengePrerenderObserver.php — registered as an Eloquent observer on IdeationChallenge. On model save or delete it marks /ideation and /ideation/{id} for cache invalidation via PrerenderService.


Tests

# PHP — run from repo root
vendor/bin/phpunit --testsuite=Laravel --filter=Ideation
vendor/bin/phpunit tests/Laravel/Feature/Controllers/IdeationChallengesControllerTest.php
vendor/bin/phpunit tests/Laravel/Feature/Controllers/AdminIdeationControllerTest.php
vendor/bin/phpunit tests/Laravel/Unit/Services/IdeationChallengeServiceTest.php
vendor/bin/phpunit tests/Laravel/Unit/Services/ChallengeOutcomeServiceTest.php
vendor/bin/phpunit tests/Laravel/Feature/GovukAlpha/IdeationParityTest.php

# React — run from react-frontend/
npm test -- --testPathPattern=ideation

Key coverage:

  • IdeationChallengesControllerTest — HTTP-level feature tests for challenge CRUD, idea submission, voting, and auth guards using DatabaseTransactions.
  • IdeationChallengeServiceTest — unit tests with mocked DB; covers pagination shape, vote toggle, and draft publish path.
  • ChallengeOutcomeServiceTest — outcome upsert and dashboard aggregation.
  • IdeationParityTest — GOV.UK accessible-frontend route integration tests.
  • React page/component tests: IdeationPage.test.tsx, ChallengeDetailPage.test.tsx, IdeaDetailPage.test.tsx, CreateChallengePage.test.tsx, IdeationAdmin.test.tsx, and the team sub-components.

Failure modes and recovery

Failure Behaviour Recovery
Feature disabled for tenant All API endpoints return HTTP 403 (FEATURE_DISABLED); React routes show a coming-soon page Enable ideation_challenges in tenant settings via the admin panel
Invalid status transition updateChallengeStatus() returns CONFLICT error; HTTP 409 Advance through the correct sequence (e.g. open → voting before voting → evaluating)
Vote on closed/evaluating challenge voteIdea() returns CONFLICT (challenge_voting_not_allowed) No action needed; the challenge must be open or voting to accept votes
Idea edit after challenge leaves open updateIdea() returns CONFLICT (challenge_closed_for_edits) Reopen the challenge status (admin) or use the admin idea-status endpoint to set the idea withdrawn
Notification email failure EmailDispatchService::sendRaw() returns false; logged as Log::warning — the primary action still succeeds Check email_logs table or SendGrid Activity for delivery status
Outcome winning_idea_id FK violation Service validates that the idea belongs to the challenge; returns VALIDATION_INVALID_VALUE Pass a valid idea ID that was submitted to the same challenge
Team conversion on non-shortlisted idea IdeaTeamConversionService checks idea eligibility; returns FORBIDDEN or CONFLICT Shortlist or mark the idea as winner before converting
Category deleted while challenges reference it ChallengeCategoryService::delete() nulls out category_id on affected challenges before deleting the category row No recovery needed; challenges retain their free-text category field