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:
87
specs/016-cancel-event/research.md
Normal file
87
specs/016-cancel-event/research.md
Normal 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.
|
||||
Reference in New Issue
Block a user