Files
fete/specs/016-cancel-event/research.md
2026-03-12 19:03:57 +01:00

5.0 KiB

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.