From 3908c89998237c747ac7c8fc0e81e93a87a34c6f Mon Sep 17 00:00:00 2001 From: nitrix Date: Thu, 12 Mar 2026 18:50:46 +0100 Subject: [PATCH] Add spec, plan, and tasks for 016-cancel-event feature Co-Authored-By: Claude Opus 4.6 --- .../checklists/requirements.md | 36 ++++ .../contracts/patch-event-endpoint.yaml | 78 ++++++++ specs/016-cancel-event/data-model.md | 82 +++++++++ specs/016-cancel-event/plan.md | 79 +++++++++ specs/016-cancel-event/quickstart.md | 48 +++++ specs/016-cancel-event/research.md | 87 +++++++++ specs/016-cancel-event/spec.md | 97 ++++++++++ specs/016-cancel-event/tasks.md | 167 ++++++++++++++++++ 8 files changed, 674 insertions(+) create mode 100644 specs/016-cancel-event/checklists/requirements.md create mode 100644 specs/016-cancel-event/contracts/patch-event-endpoint.yaml create mode 100644 specs/016-cancel-event/data-model.md create mode 100644 specs/016-cancel-event/plan.md create mode 100644 specs/016-cancel-event/quickstart.md create mode 100644 specs/016-cancel-event/research.md create mode 100644 specs/016-cancel-event/spec.md create mode 100644 specs/016-cancel-event/tasks.md diff --git a/specs/016-cancel-event/checklists/requirements.md b/specs/016-cancel-event/checklists/requirements.md new file mode 100644 index 0000000..7082132 --- /dev/null +++ b/specs/016-cancel-event/checklists/requirements.md @@ -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). diff --git a/specs/016-cancel-event/contracts/patch-event-endpoint.yaml b/specs/016-cancel-event/contracts/patch-event-endpoint.yaml new file mode 100644 index 0000000..7fe111b --- /dev/null +++ b/specs/016-cancel-event/contracts/patch-event-endpoint.yaml @@ -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 diff --git a/specs/016-cancel-event/data-model.md b/specs/016-cancel-event/data-model.md new file mode 100644 index 0000000..778096b --- /dev/null +++ b/specs/016-cancel-event/data-model.md @@ -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 + + + + + + + + +``` + +## 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. diff --git a/specs/016-cancel-event/plan.md b/specs/016-cancel-event/plan.md new file mode 100644 index 0000000..7901e4f --- /dev/null +++ b/specs/016-cancel-event/plan.md @@ -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. diff --git a/specs/016-cancel-event/quickstart.md b/specs/016-cancel-event/quickstart.md new file mode 100644 index 0000000..69ca01a --- /dev/null +++ b/specs/016-cancel-event/quickstart.md @@ -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) diff --git a/specs/016-cancel-event/research.md b/specs/016-cancel-event/research.md new file mode 100644 index 0000000..169fe54 --- /dev/null +++ b/specs/016-cancel-event/research.md @@ -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. diff --git a/specs/016-cancel-event/spec.md b/specs/016-cancel-event/spec.md new file mode 100644 index 0000000..9c78487 --- /dev/null +++ b/specs/016-cancel-event/spec.md @@ -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). diff --git a/specs/016-cancel-event/tasks.md b/specs/016-cancel-event/tasks.md new file mode 100644 index 0000000..9127f28 --- /dev/null +++ b/specs/016-cancel-event/tasks.md @@ -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 (T006–T011) can run in parallel +- All US2 test tasks (T017–T021) can run in parallel +- US1 backend (T012–T014) and US2 backend (T022) can run in parallel after Setup +- US1 frontend (T015–T016) and US2 frontend (T023–T025) 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