Add spec, plan, and tasks for 016-cancel-event feature

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-12 18:50:46 +01:00
parent bf0f4ffb7f
commit 3908c89998
8 changed files with 674 additions and 0 deletions

View File

@@ -0,0 +1,36 @@
# Specification Quality Checklist: Cancel Event
**Purpose**: Validate specification completeness and quality before proceeding to planning
**Created**: 2026-03-12
**Feature**: [spec.md](../spec.md)
## Content Quality
- [x] No implementation details (languages, frameworks, APIs)
- [x] Focused on user value and business needs
- [x] Written for non-technical stakeholders
- [x] All mandatory sections completed
## Requirement Completeness
- [x] No [NEEDS CLARIFICATION] markers remain
- [x] Requirements are testable and unambiguous
- [x] Success criteria are measurable
- [x] Success criteria are technology-agnostic (no implementation details)
- [x] All acceptance scenarios are defined
- [x] Edge cases are identified
- [x] Scope is clearly bounded
- [x] Dependencies and assumptions identified
## Feature Readiness
- [x] All functional requirements have clear acceptance criteria
- [x] User scenarios cover primary flows
- [x] Feature meets measurable outcomes defined in Success Criteria
- [x] No implementation details leak into specification
## Notes
- All items pass. Spec is ready for `/speckit.clarify` or `/speckit.plan`.
- Feature scope is deliberately tight: cancel + display. No notifications, no undo, no event-list changes.
- Both user stories are P1 because they are two sides of the same coin (cancel action + display result).

View File

@@ -0,0 +1,78 @@
# OpenAPI additions for Cancel Event feature
# To be merged into backend/src/main/resources/openapi/api.yaml
# PATCH method added to existing /events/{eventToken} path
# Under paths./events/{eventToken}:
# --- Add PATCH method to existing path ---
# /events/{eventToken}:
# patch:
# operationId: patchEvent
# summary: Update an event (currently: cancel)
# description: |
# Partial update of an event resource. Currently the only supported operation
# is cancellation (setting cancelled to true). Requires the organizer token.
# Cancellation is irreversible.
# tags: [Events]
# parameters:
# - $ref: '#/components/parameters/EventToken'
# requestBody:
# required: true
# content:
# application/json:
# schema:
# $ref: '#/components/schemas/PatchEventRequest'
# responses:
# '204':
# description: Event updated successfully
# '403':
# description: Invalid organizer token
# content:
# application/json:
# schema:
# $ref: '#/components/schemas/ErrorResponse'
# '404':
# description: Event not found
# content:
# application/json:
# schema:
# $ref: '#/components/schemas/ErrorResponse'
# '409':
# description: Event is already cancelled
# content:
# application/json:
# schema:
# $ref: '#/components/schemas/ErrorResponse'
# --- New schemas ---
# PatchEventRequest:
# type: object
# required: [organizerToken, cancelled]
# properties:
# organizerToken:
# type: string
# format: uuid
# description: The organizer token proving ownership of the event
# example: "550e8400-e29b-41d4-a716-446655440001"
# cancelled:
# type: boolean
# description: Set to true to cancel the event (irreversible)
# example: true
# cancellationReason:
# type: string
# maxLength: 2000
# description: Optional cancellation reason
# example: "Unfortunately the venue is no longer available."
# --- Extended schema: GetEventResponse ---
# Add to existing GetEventResponse properties:
# cancelled:
# type: boolean
# description: Whether the event has been cancelled
# example: false
# cancellationReason:
# type: string
# nullable: true
# description: Reason for cancellation, if provided
# example: null

View File

@@ -0,0 +1,82 @@
# Data Model: Cancel Event
**Feature Branch**: `016-cancel-event` | **Date**: 2026-03-12
## Entity Changes
### Event (extended)
Two new fields added to the existing Event entity:
| Field | Type | Constraints | Description |
|--------------------|----------------|------------------------------|--------------------------------------------------|
| cancelled | boolean | NOT NULL, DEFAULT false | Whether the event has been cancelled |
| cancellationReason | String (2000) | Nullable | Optional reason provided by organizer |
### State Transition
```
ACTIVE ──cancel()──► CANCELLED
```
- One-way transition only. No path from CANCELLED back to ACTIVE.
- `cancel()` sets `cancelled = true` and optionally sets `cancellationReason`.
- Once cancelled, the event remains visible but RSVP creation is blocked.
### Validation Rules
- `cancellationReason` max length: 2000 characters (matches description field).
- `cancellationReason` is plain text only (no HTML/markdown).
- `cancelled` can only transition from `false` to `true`, never back.
- Existing RSVPs are preserved when an event is cancelled (no cascade).
## Database Migration (Liquibase Changeset 004)
```xml
<changeSet id="004-add-cancellation-columns" author="fete">
<addColumn tableName="events">
<column name="cancelled" type="BOOLEAN" defaultValueBoolean="false">
<constraints nullable="false"/>
</column>
<column name="cancellation_reason" type="VARCHAR(2000)"/>
</addColumn>
</changeSet>
```
## Domain Model Impact
### Event.java (domain)
Add fields:
```java
private boolean cancelled;
private String cancellationReason;
```
Add method:
```java
public void cancel(String reason) {
if (this.cancelled) {
throw new EventAlreadyCancelledException();
}
this.cancelled = true;
this.cancellationReason = reason;
}
```
### EventJpaEntity.java (persistence)
Add columns:
```java
@Column(name = "cancelled", nullable = false)
private boolean cancelled;
@Column(name = "cancellation_reason", length = 2000)
private String cancellationReason;
```
## RSVP Impact
- `POST /events/{eventToken}/rsvps` must check `event.isCancelled()` before accepting.
- If cancelled → return `409 Conflict`.
- Existing RSVPs remain untouched — no delete, no status change.

View File

@@ -0,0 +1,79 @@
# Implementation Plan: Cancel Event
**Branch**: `016-cancel-event` | **Date**: 2026-03-12 | **Spec**: [spec.md](spec.md)
**Input**: Feature specification from `/specs/016-cancel-event/spec.md`
## Summary
Allow organizers to permanently cancel events via a bottom sheet UI. Cancelled events display a red banner to visitors and block new RSVPs. Implementation adds a `PATCH /events/{eventToken}` endpoint, extends the Event entity with `cancelled` and `cancellationReason` fields, and reuses the existing `BottomSheet.vue` component for the cancel interaction.
## Technical Context
**Language/Version**: Java 25 (backend), TypeScript 5.9 (frontend)
**Primary Dependencies**: Spring Boot 3.5.x, Vue 3, Vue Router 5, openapi-fetch, openapi-typescript
**Storage**: PostgreSQL (JPA via Spring Data, Liquibase migrations)
**Testing**: JUnit (backend), Vitest (frontend unit), Playwright + MSW (frontend E2E)
**Target Platform**: Self-hosted Linux server, mobile-first PWA
**Project Type**: Web application (REST API + SPA)
**Performance Goals**: N/A — simple state transition, no performance-critical path
**Constraints**: Privacy by design (no analytics/tracking), WCAG AA, mobile-first
**Scale/Scope**: Single new endpoint, 2 new DB columns, 1 view extension
## Constitution Check
*GATE: Must pass before Phase 0 research. Re-check after Phase 1 design.*
| Principle | Status | Notes |
|-----------|--------|-------|
| I. Privacy by Design | PASS | No new data collection beyond organizer-provided reason. No analytics. |
| II. Test-Driven Methodology | PASS | TDD enforced: tests before implementation, E2E mandatory for both user stories. |
| III. API-First Development | PASS | OpenAPI spec updated first, types generated before implementation. `example:` fields included. |
| IV. Simplicity & Quality | PASS | Minimal change: 2 columns, 1 endpoint, reuse existing BottomSheet. No over-engineering. |
| V. Dependency Discipline | PASS | No new dependencies required. |
| VI. Accessibility | PASS | Reuses accessible BottomSheet component. Banner uses semantic HTML + ARIA. |
**Gate result: PASS** — no violations.
## Project Structure
### Documentation (this feature)
```text
specs/016-cancel-event/
├── plan.md # This file
├── spec.md # Feature specification
├── research.md # Phase 0 output — design decisions
├── data-model.md # Phase 1 output — entity changes
├── quickstart.md # Phase 1 output — implementation overview
├── contracts/ # Phase 1 output — API contract additions
│ └── patch-event-endpoint.yaml
└── tasks.md # Phase 2 output (/speckit.tasks command)
```
### Source Code (repository root)
```text
backend/
├── src/main/java/de/fete/
│ ├── domain/model/Event.java # + cancelled, cancellationReason, cancel()
│ ├── application/service/EventService.java # + CancelEventUseCase implementation
│ ├── adapter/in/web/EventController.java # + cancelEvent endpoint
│ └── adapter/out/persistence/
│ ├── EventJpaEntity.java # + cancelled, cancellation_reason columns
│ └── EventPersistenceAdapter.java # + mapper updates
├── src/main/resources/
│ ├── openapi/api.yaml # + cancel endpoint, request/response schemas
│ └── db/changelog/004-add-cancellation-columns.xml # New migration
└── src/test/java/de/fete/ # Unit + integration tests
frontend/
├── src/
│ └── views/EventDetailView.vue # + cancel button, bottom sheet, banner
└── e2e/ # E2E tests for both user stories
```
**Structure Decision**: Web application (Option 2) — matches existing project layout with `backend/` and `frontend/` at repository root.
## Complexity Tracking
> No violations — table not applicable.

View File

@@ -0,0 +1,48 @@
# Quickstart: Cancel Event
**Feature Branch**: `016-cancel-event`
## What This Feature Does
Adds the ability for an organizer to permanently cancel an event. Cancelled events display a red banner to visitors and block new RSVPs.
## Implementation Scope
### Backend
1. **Liquibase migration** (003): Add `cancelled` (boolean) and `cancellation_reason` (varchar 2000) columns to `events` table.
2. **Domain model**: Extend `Event.java` with `cancelled` and `cancellationReason` fields + `cancel()` method.
3. **JPA entity**: Extend `EventJpaEntity.java` with matching columns and mapper updates.
4. **OpenAPI spec**: Add `PATCH /events/{eventToken}` endpoint + extend `GetEventResponse` with cancellation fields.
5. **Use case**: New `CancelEventUseCase` interface + implementation in `EventService`.
6. **Controller**: Implement `cancelEvent` in `EventController`.
7. **RSVP guard**: Add cancelled check to RSVP creation (return 409).
### Frontend
1. **Cancel bottom sheet**: Add cancel button (organizer-only) + bottom sheet with textarea and confirm button in `EventDetailView.vue`.
2. **Cancellation banner**: Red banner at top of event detail when `cancelled === true`.
3. **RSVP hiding**: Hide `RsvpBar` when event is cancelled.
4. **API client**: Use generated types from updated OpenAPI spec.
### Testing
1. **Backend unit tests**: Cancel use case, RSVP rejection on cancelled events.
2. **Backend integration tests**: Full cancel flow via API.
3. **Frontend unit tests**: Cancel bottom sheet, banner display, RSVP hiding.
4. **E2E tests**: Organizer cancels event, attendee sees cancelled event.
## Key Files to Modify
| File | Change |
|------|--------|
| `backend/src/main/resources/openapi/api.yaml` | New endpoint + schema extensions |
| `backend/src/main/resources/db/changelog/` | New changeset 003 |
| `backend/src/main/java/de/fete/domain/model/Event.java` | Add cancelled fields + cancel() |
| `backend/src/main/java/de/fete/adapter/out/persistence/EventJpaEntity.java` | Add columns |
| `backend/src/main/java/de/fete/application/service/EventService.java` | Implement cancel |
| `backend/src/main/java/de/fete/adapter/in/web/EventController.java` | Implement endpoint |
| `frontend/src/views/EventDetailView.vue` | Cancel button, bottom sheet, banner |
## Prerequisites
- Existing RSVP bottom sheet pattern (already implemented)
- Organizer token stored in localStorage (already implemented)
- `BottomSheet.vue` component (already exists)

View File

@@ -0,0 +1,87 @@
# Research: Cancel Event
**Feature Branch**: `016-cancel-event` | **Date**: 2026-03-12
## Decision 1: API Endpoint Design
**Decision**: Use `PATCH /events/{eventToken}` with organizer token and cancellation fields in request body.
**Rationale**:
- PATCH is standard REST for partial resource updates — cancellation is a state change on the event resource.
- The event is not removed, so DELETE is not appropriate. The event remains visible with a cancellation banner.
- The organizer token is sent in the request body to keep it out of URL/query strings and server access logs.
- Request body: `{ "organizerToken": "uuid", "cancelled": true, "cancellationReason": "optional string" }`.
- Response: `204 No Content` on success.
- Error responses: `404` if event not found, `403` if organizer token is wrong, `409` if already cancelled.
- Currently the only supported PATCH operation is cancellation. The endpoint validates that `cancelled` is `true` and rejects requests that attempt to set other fields.
**Alternatives considered**:
- `POST /events/{eventToken}/cancel` — rejected because a dedicated sub-resource endpoint is RPC-style, not RESTful. PATCH on the resource itself is the standard approach.
- `DELETE /events/{eventToken}` — rejected because the event is not deleted, it remains visible with a cancellation banner.
## Decision 2: Database Schema Extension
**Decision**: Add two columns to the `events` table: `cancelled BOOLEAN NOT NULL DEFAULT FALSE` and `cancellation_reason VARCHAR(2000)`.
**Rationale**:
- Boolean flag is the simplest representation of the cancelled state.
- 2000 chars matches the existing description field limit — consistent and generous.
- DEFAULT FALSE ensures backward compatibility with existing rows.
- A Liquibase changeset (003) adds both columns.
**Alternatives considered**:
- Enum status field (`ACTIVE`, `CANCELLED`) — rejected as over-engineering for a binary state with no other planned transitions.
- Separate cancellation table — rejected as unnecessary complexity for two columns.
## Decision 3: RSVP Blocking on Cancelled Events
**Decision**: The RSVP creation endpoint (`POST /events/{eventToken}/rsvps`) checks the event's cancelled flag and returns `409 Conflict` if the event is cancelled.
**Rationale**:
- Server-side enforcement is required (FR-006) — frontend hiding the button is not sufficient.
- 409 Conflict is semantically correct: the request conflicts with the current state of the resource.
- Existing RSVPs are preserved (FR-007) — no cascade or cleanup needed.
**Alternatives considered**:
- 400 Bad Request — rejected because the request itself is well-formed; the conflict is with resource state.
- 422 Unprocessable Entity — rejected because the issue is not validation but state conflict.
## Decision 4: Frontend Cancel Bottom Sheet
**Decision**: Reuse the existing `BottomSheet.vue` component. Add cancel-specific content (textarea + confirm button) directly in `EventDetailView.vue`, similar to how the RSVP form is embedded.
**Rationale**:
- The spec explicitly requires the bottom sheet pattern consistent with RSVP flow (FR-002).
- `BottomSheet.vue` is already a generic, accessible, glassmorphism-styled container.
- No need for a separate component — the cancel form is simple (textarea + button + error message).
- Error handling follows the same pattern as RSVP: inline error in the sheet, button re-enabled.
**Alternatives considered**:
- Separate `CancelBottomSheet.vue` component — rejected as unnecessary extraction for a simple form.
- ConfirmDialog instead of BottomSheet — rejected because spec explicitly requires bottom sheet.
## Decision 5: Organizer Token Authorization
**Decision**: The cancel endpoint receives the organizer token in the request body. The frontend retrieves it from localStorage via `useEventStorage.getOrganizerToken()`.
**Rationale**:
- Consistent with how organizer identity works throughout the app — token-based, no auth system.
- The organizer token is already stored in localStorage when the event is created.
- Body parameter keeps the token out of URL/query strings and server access logs.
**Alternatives considered**:
- Authorization header — rejected because there's no auth system; the organizer token is not a session token.
- Query parameter — rejected to keep token out of server logs (same reason the attendee endpoint should eventually be migrated away from query params).
## Decision 6: GetEventResponse Extension
**Decision**: Add `cancelled: boolean` and `cancellationReason: string | null` to the `GetEventResponse` schema.
**Rationale**:
- The frontend needs to know whether an event is cancelled to show the banner and hide RSVP buttons.
- Both fields are always returned (no separate endpoint needed).
- `cancelled` defaults to `false` for existing events.
**Alternatives considered**:
- Separate endpoint for cancellation status — rejected as unnecessary network overhead.
- Only return cancellation info for cancelled events — rejected because the frontend needs the boolean regardless to decide UI state.

View File

@@ -0,0 +1,97 @@
# Feature Specification: Cancel Event
**Feature Branch**: `016-cancel-event`
**Created**: 2026-03-12
**Status**: Draft
**Input**: User description: "As an organizer, I want to cancel an event so that attendees know the event will not take place"
## User Scenarios & Testing *(mandatory)*
### User Story 1 - Organizer Cancels an Event (Priority: P1)
An organizer navigates to their event page (identified by having the organizer token). They decide to cancel the event. They tap a "Cancel Event" button, which opens a bottom sheet (visually similar to the attend/RSVP flow). The bottom sheet contains a text area for an optional cancellation reason and a confirm button. After confirming, the event is permanently marked as cancelled. This action is irreversible.
**Why this priority**: This is the core action of the feature. Without it, nothing else matters.
**Independent Test**: Can be fully tested by viewing the event with the organizer token, tapping cancel, optionally entering a reason, and confirming. The event's cancelled state persists on reload.
**Acceptance Scenarios**:
1. **Given** an active event viewed by someone who has the organizer token, **When** the organizer taps "Cancel Event", **Then** a bottom sheet opens with a text area and a confirm button.
2. **Given** the cancel bottom sheet is open, **When** the organizer enters a cancellation reason and taps the confirm button, **Then** the event is marked as cancelled with the provided reason.
3. **Given** the cancel bottom sheet is open, **When** the organizer taps confirm without entering a reason, **Then** the event is marked as cancelled without a reason.
4. **Given** a cancelled event, **When** the organizer revisits the event page, **Then** the event remains cancelled (irreversible).
5. **Given** the cancel bottom sheet is open, **When** the organizer taps confirm and the API call fails, **Then** an error message is displayed in the bottom sheet, the sheet remains open, and the confirm button is re-enabled for retry.
---
### User Story 2 - Attendee Sees Cancelled Event (Priority: P1)
An attendee (or any visitor) opens the event detail page for a cancelled event. A prominent red banner is displayed at the top of the page, clearly communicating that the event has been cancelled. If the organizer provided a cancellation reason, it is shown within the banner. The RSVP buttons are hidden — no new RSVPs can be submitted.
**Why this priority**: Equal to P1 because the cancellation must be visible to attendees for the feature to deliver value. Without this, cancelling has no effect from the attendee's perspective.
**Independent Test**: Can be tested by viewing a cancelled event's detail page and verifying the banner appears, the reason is displayed (if provided), and RSVP buttons are hidden.
**Acceptance Scenarios**:
1. **Given** a cancelled event with a cancellation reason, **When** a visitor opens the event detail page, **Then** a prominent red banner is displayed showing that the event is cancelled along with the reason.
2. **Given** a cancelled event without a cancellation reason, **When** a visitor opens the event detail page, **Then** a prominent red banner is displayed showing that the event is cancelled, without a reason text.
3. **Given** a cancelled event, **When** a visitor opens the event detail page, **Then** the RSVP buttons are not visible.
4. **Given** a cancelled event, **When** a visitor opens the event detail page, **Then** all other event details (title, date, location, description) remain visible.
---
### Edge Cases
- What happens when the organizer tries to cancel an already cancelled event? The cancel button is not available on an already cancelled event.
- What happens to existing RSVPs when an event is cancelled? They are preserved as-is but no new RSVPs can be submitted.
- What happens when the event is both cancelled and expired? The auto-delete mechanism (feature 013) continues to apply normally — cancelled events are deleted on the same schedule as non-cancelled events.
- What happens when the cancellation API call fails (network error, server error)? The bottom sheet remains open, a visible error message is displayed within the sheet, and the confirm button is re-enabled so the organizer can retry.
- How are cancelled events displayed in the event list? Out of scope for this feature — the event list view is not affected.
## Requirements *(mandatory)*
### Functional Requirements
- **FR-001**: System MUST allow the organizer to cancel an event via the organizer view.
- **FR-002**: The cancellation interaction MUST use a bottom sheet pattern consistent with the existing RSVP/attend flow.
- **FR-003**: The bottom sheet MUST contain a text area for an optional cancellation reason and a confirm button.
- **FR-004**: Cancellation MUST be irreversible — once cancelled, there is no way to undo it.
- **FR-005**: System MUST store a cancelled flag and an optional cancellation reason for the event.
- **FR-006**: System MUST NOT allow new RSVPs for a cancelled event.
- **FR-007**: System MUST preserve existing RSVPs when an event is cancelled.
- **FR-008**: The event detail page MUST display a prominent red banner for cancelled events.
- **FR-009**: The banner MUST include the cancellation reason when one was provided.
- **FR-010**: The RSVP buttons MUST be hidden on a cancelled event's detail page.
- **FR-011**: All other event information MUST remain visible on a cancelled event's detail page.
- **FR-012**: The cancel button MUST NOT be shown on an already cancelled event.
- **FR-013**: There MUST be no push notifications, emails, or any active notification mechanism for cancellations.
- **FR-014**: If the cancellation API call fails, the bottom sheet MUST remain open, display an error message, and allow the organizer to retry.
- **FR-015**: Changes to the event list view for cancelled events are explicitly OUT OF SCOPE for this feature.
### Key Entities
- **Event** (extended): Gains a cancelled state (boolean) and an optional cancellation reason (free text). An event can transition from active to cancelled, but not back.
## Success Criteria *(mandatory)*
### Measurable Outcomes
- **SC-001**: Organizer can cancel an event in under 30 seconds (open bottom sheet, optionally type reason, confirm).
- **SC-002**: 100% of visitors to a cancelled event's detail page see the cancellation banner without scrolling.
- **SC-003**: 0% of cancelled events accept new RSVPs.
- **SC-004**: Existing RSVPs are fully preserved after cancellation (no data loss).
## Clarifications
### Session 2026-03-12
- Q: How should cancelled events appear in the event list view? → A: Out of scope for this feature — event list view is not affected.
- Q: What should happen when the cancellation API call fails? → A: Error message displayed in the bottom sheet, sheet remains open, confirm button re-enabled for retry.
## Assumptions
- The bottom sheet component pattern already exists from the RSVP/attend flow and can be reused.
- The cancellation reason has a maximum length of 2000 characters (consistent with the event description field).
- The cancellation reason is plain text (no formatting or markup).

View File

@@ -0,0 +1,167 @@
# Tasks: Cancel Event
**Input**: Design documents from `/specs/016-cancel-event/`
**Prerequisites**: plan.md, spec.md, research.md, data-model.md, contracts/patch-event-endpoint.yaml
**Tests**: Included — constitution mandates TDD (tests before implementation) and E2E for every frontend user story.
**Organization**: Tasks grouped by user story. Both stories are P1 but US2 depends on US1's backend work.
## Format: `[ID] [P?] [Story] Description`
- **[P]**: Can run in parallel (different files, no dependencies)
- **[Story]**: Which user story this task belongs to (e.g., US1, US2)
- Include exact file paths in descriptions
---
## Phase 1: Setup (Shared Infrastructure)
**Purpose**: OpenAPI spec, database migration, and domain model changes that both user stories depend on.
- [ ] T001 Update OpenAPI spec with PATCH endpoint on `/events/{eventToken}`, PatchEventRequest schema (`organizerToken`, `cancelled`, `cancellationReason`), and extend GetEventResponse with `cancelled`/`cancellationReason` fields in `backend/src/main/resources/openapi/api.yaml`
- [ ] T002 Add Liquibase changeset 004 adding `cancelled` (BOOLEAN NOT NULL DEFAULT FALSE) and `cancellation_reason` (VARCHAR 2000) columns to `events` table in `backend/src/main/resources/db/changelog/004-add-cancellation-columns.xml` and register it in `db.changelog-master.xml`
- [ ] T003 [P] Extend domain model `Event.java` with `cancelled`, `cancellationReason` fields and `cancel(String reason)` method (throws `EventAlreadyCancelledException`). Create `EventAlreadyCancelledException` in `backend/src/main/java/de/fete/application/service/` following the pattern of existing exceptions. Domain model: `backend/src/main/java/de/fete/domain/model/Event.java`
- [ ] T004 [P] Extend JPA entity `EventJpaEntity.java` with `cancelled` and `cancellation_reason` columns and update mapper in `backend/src/main/java/de/fete/adapter/out/persistence/EventJpaEntity.java`
- [ ] T005 Regenerate frontend TypeScript types from updated OpenAPI spec via `cd frontend && npm run generate-types` (or equivalent openapi-typescript command)
**Checkpoint**: Schema, migration, and domain model ready. Both user stories can now proceed.
---
## Phase 2: User Story 1 — Organizer Cancels an Event (Priority: P1) MVP
**Goal**: Organizer can cancel an event via a bottom sheet with optional reason. Cancellation is irreversible and persists on reload.
**Independent Test**: View event with organizer token, tap cancel, optionally enter reason, confirm. Event remains cancelled on reload.
### Tests for User Story 1
> **Write these tests FIRST — ensure they FAIL before implementation**
- [ ] T006 [P] [US1] Write unit test for `cancel()` domain method (happy path, already-cancelled throws) in `backend/src/test/java/de/fete/domain/model/EventTest.java`
- [ ] T007 [P] [US1] Write unit test for cancel use case in EventService (delegates to domain, saves, 403/404/409 cases) in `backend/src/test/java/de/fete/application/service/EventServiceCancelTest.java`
- [ ] T008 [P] [US1] Write integration test for `PATCH /events/{eventToken}` endpoint (204 success, 403 wrong token, 404 not found, 409 already cancelled) in `backend/src/test/java/de/fete/adapter/in/web/EventControllerCancelTest.java`
- [ ] T009 [P] [US1] Write E2E test: organizer opens cancel bottom sheet, enters reason, confirms — event shows as cancelled on reload in `frontend/e2e/cancel-event.spec.ts`
- [ ] T010 [P] [US1] Write E2E test: organizer cancels without reason — event shows as cancelled in `frontend/e2e/cancel-event.spec.ts`
- [ ] T011 [P] [US1] Write E2E test: cancel API fails — error displayed in bottom sheet, button re-enabled for retry in `frontend/e2e/cancel-event.spec.ts`
### Implementation for User Story 1
- [ ] T012 [US1] Create `CancelEventUseCase` interface (or add method to existing use case interface) in `backend/src/main/java/de/fete/domain/port/in/`
- [ ] T013 [US1] Implement cancel logic in `EventService.java` — load event, verify organizer token, call `event.cancel(reason)`, persist in `backend/src/main/java/de/fete/application/service/EventService.java`
- [ ] T014 [US1] Implement `cancelEvent` endpoint in `EventController.java` — PATCH handler, request body binding, error mapping (403/404/409) in `backend/src/main/java/de/fete/adapter/in/web/EventController.java`
- [ ] T015 [US1] Add cancel button (visible only when organizer token exists and event not cancelled — covers FR-012) and cancel bottom sheet (textarea with 2000 char limit + confirm button + inline error) to `frontend/src/views/EventDetailView.vue`
- [ ] T016 [US1] Wire cancel bottom sheet confirm action to `PATCH /events/{eventToken}` API call via openapi-fetch, handle success (reload event data) and error (show inline message, re-enable button) in `frontend/src/views/EventDetailView.vue`
**Checkpoint**: Organizer can cancel an event. All US1 acceptance scenarios pass.
---
## Phase 3: User Story 2 — Attendee Sees Cancelled Event (Priority: P1)
**Goal**: Visitors see a prominent red cancellation banner on cancelled events. RSVP buttons are hidden. All other event details remain visible.
**Independent Test**: View a cancelled event's detail page — banner visible (with reason if provided), RSVP buttons hidden, other details intact.
### Tests for User Story 2
> **Write these tests FIRST — ensure they FAIL before implementation**
- [ ] T017 [P] [US2] Write unit test for RSVP creation rejection on cancelled events (409 Conflict) in `backend/src/test/java/de/fete/application/service/RsvpServiceCancelledTest.java`
- [ ] T018 [P] [US2] Write integration test for `POST /events/{eventToken}/rsvps` returning 409 when event is cancelled in `backend/src/test/java/de/fete/adapter/in/web/RsvpControllerCancelledTest.java`
- [ ] T019 [P] [US2] Write E2E test: visitor sees red banner with cancellation reason on cancelled event in `frontend/e2e/cancelled-event-visitor.spec.ts`
- [ ] T020 [P] [US2] Write E2E test: visitor sees red banner without reason when no reason was provided in `frontend/e2e/cancelled-event-visitor.spec.ts`
- [ ] T021 [P] [US2] Write E2E test: RSVP buttons hidden on cancelled event, other details remain visible in `frontend/e2e/cancelled-event-visitor.spec.ts`
### Implementation for User Story 2
- [ ] T022 [US2] Add cancelled-event guard to RSVP creation — check `event.isCancelled()`, return 409 Conflict in `backend/src/main/java/de/fete/application/service/RsvpService.java`
- [ ] T023 [US2] Add cancellation banner component/section (red, prominent, includes reason if present, WCAG AA contrast) to `frontend/src/views/EventDetailView.vue`
- [ ] T024 [US2] Hide RSVP buttons (`RsvpBar` or equivalent) when `event.cancelled === true` in `frontend/src/views/EventDetailView.vue`
- [ ] ~~T025~~ Merged into T015 (cancel button v-if already handles FR-012)
**Checkpoint**: Both user stories fully functional. All acceptance scenarios pass.
---
## Phase 4: Polish & Cross-Cutting Concerns
**Purpose**: Validation, edge cases, and final cleanup.
- [ ] T026 Verify cancellationReason max length (2000 chars) is enforced at API level (OpenAPI `maxLength`), domain level in `Event.java`, and UI level (textarea maxlength/counter)
- [ ] T027 Run full backend test suite (`cd backend && ./mvnw verify`) and fix any failures
- [ ] T028 Run full frontend test suite (`cd frontend && npm run test:unit`) and fix any failures
- [ ] T029 Run E2E tests (`cd frontend && npx playwright test`) and fix any failures
- [ ] T030 Run backend checkstyle (`cd backend && ./mvnw checkstyle:check`) and fix violations
---
## Dependencies & Execution Order
### Phase Dependencies
- **Setup (Phase 1)**: No dependencies — start immediately
- **US1 (Phase 2)**: Depends on Setup completion
- **US2 (Phase 3)**: Depends on Setup completion; backend RSVP guard is independent of US1, but frontend banner/hiding can be built in parallel with US1
- **Polish (Phase 4)**: Depends on all user stories complete
### User Story Dependencies
- **US1 (P1)**: Can start after Phase 1 (Setup). No dependency on US2.
- **US2 (P1)**: Can start after Phase 1 (Setup). Backend RSVP guard is independent of US1. Frontend banner/hiding only needs the `cancelled`/`cancellationReason` fields in GetEventResponse (from Setup).
### Within Each User Story
- Tests MUST be written and FAIL before implementation
- Domain model before service layer
- Service layer before controller/endpoint
- Backend before frontend (API must exist before UI wires to it)
- Core implementation before error handling polish
### Parallel Opportunities
- T003 + T004 (domain model + JPA entity) can run in parallel
- All US1 test tasks (T006T011) can run in parallel
- All US2 test tasks (T017T021) can run in parallel
- US1 backend (T012T014) and US2 backend (T022) can run in parallel after Setup
- US1 frontend (T015T016) and US2 frontend (T023T025) can run in parallel if backend is ready
---
## Parallel Example: User Story 1
```bash
# Launch all US1 tests together (TDD - write first, expect failures):
Task: T006 "Unit test for cancel() domain method"
Task: T007 "Unit test for cancel use case in EventService"
Task: T008 "Integration test for PATCH /events/{eventToken}"
Task: T009 "E2E test: organizer cancels with reason"
Task: T010 "E2E test: organizer cancels without reason"
Task: T011 "E2E test: cancel API failure handling"
# Then implement sequentially:
Task: T012 "CancelEventUseCase interface"
Task: T013 "EventService cancel implementation"
Task: T014 "EventController cancelEvent endpoint"
Task: T015 "Cancel button + bottom sheet in EventDetailView"
Task: T016 "Wire cancel action to API"
```
---
## Implementation Strategy
### MVP First (User Story 1 Only)
1. Complete Phase 1: Setup (OpenAPI, migration, domain model)
2. Complete Phase 2: US1 — Organizer cancels event
3. **STOP and VALIDATE**: All US1 acceptance scenarios pass
4. Deploy/demo if ready — cancellation works end-to-end
### Incremental Delivery
1. Setup → Shared infrastructure ready
2. US1 → Organizer can cancel → Test → Deploy (MVP!)
3. US2 → Attendees see cancellation + RSVP blocked → Test → Deploy
4. Polish → Full verification, checkstyle, edge cases