Add 008-rsvp feature spec and design artifacts
Spec, research decisions, implementation plan, data model, API contract, and task breakdown for the RSVP feature. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -5,11 +5,25 @@
|
||||
**Status**: Draft
|
||||
**Source**: Migrated from spec/userstories.md
|
||||
|
||||
## Clarifications
|
||||
|
||||
### Session 2026-03-06
|
||||
|
||||
- Q: How should the server deduplicate RSVPs from the same device without accounts? → A: Server generates an `rsvpToken` (UUID) on first RSVP, returns it to the client. Client stores it in localStorage per event. On re-RSVP, client sends the `rsvpToken` to update instead of create. No global device identifier — the token is event-scoped. If localStorage is lost (cleared or different device), a duplicate entry is accepted as a privacy trade-off.
|
||||
- Q: Should typed token value objects be used in the backend? → A: Yes. Backend: The three token types (EventToken, OrganizerToken, RsvpToken) MUST be modeled as distinct Java record types wrapping UUID, not passed as raw UUID values. Frontend: No branded types — plain string variables with clear naming (eventToken, rsvpToken) are sufficient given TypeScript's structural typing and OpenAPI codegen.
|
||||
- Q: How should the RSVP interaction be presented on the event page? → A: Fullscreen event presentation (gradient background, later Unsplash). Title prominent at top, key facts below (description, date, attendee count) — spacious layout. Sticky bottom bar with RSVP CTA. Tap opens a bottom sheet with the RSVP form. After RSVP, the bar shows status ("Du kommst!" + edit option) instead of the CTA.
|
||||
- Q: How does the RSVP form handle declining? → A: There is no explicit "not attending" button. The bottom sheet only offers the attending flow (name + submit). To not attend, the guest simply closes the sheet. Withdrawing an existing RSVP (DELETE with rsvpToken) is out of scope — deferred to a separate edit-RSVP spec.
|
||||
- Q: Should the attendee name list be publicly visible on the event page? → A: No. Only the attendee count is shown publicly. The full name list is visible only to the organizer (via organizer link). This maximizes guest privacy.
|
||||
- Q: Should the RSVP endpoint have spam/abuse protection? → A: No. The RSVP endpoint is intentionally unprotected — risk is consciously accepted as a privacy trade-off consistent with the no-account, no-tracking philosophy. Protection measures can be retrofitted in a separate spec if real-world abuse occurs. KISS.
|
||||
- Q: How is the attendee count delivered and updated? → A: As a new `attendeeCount` field in the existing Event response (no separate endpoint). Loaded once on page load, no polling or WebSocket. After the guest's own RSVP submission, the count is optimistically incremented (+1) client-side. KISS.
|
||||
- Q: What determines the RSVP cutoff? → A: The event date itself. No separate expiry field. After the event date has passed, RSVPs are blocked (form hidden, server rejects).
|
||||
- Q: Should the RSVP entity have an `attending` boolean field? → A: No. The server only stores attending RSVPs — existence of an entry implies attendance. No `attending` boolean needed. Deletion of entries (withdrawal) is deferred to the edit-RSVP spec.
|
||||
|
||||
## User Scenarios & Testing
|
||||
|
||||
### User Story 1 - Submit an RSVP (Priority: P1)
|
||||
|
||||
A guest opens an active event page and indicates whether they will attend. If attending, they must provide their name. If not attending, the name is optional. The RSVP is sent to the server and persisted. The guest's choice, name, event token, title, and date are saved in localStorage.
|
||||
A guest opens an active event page, which presents the event fullscreen (gradient background, title prominent at top, key facts below including attendee count). A sticky bottom bar shows an RSVP call-to-action. Tapping opens a bottom sheet with the RSVP form: name field + submit. The RSVP is sent to the server and persisted. The server returns an rsvpToken which, along with the name, event token, title, and date, is saved in localStorage. After submission, the bottom sheet closes and the sticky bar shows the guest's RSVP status. To not attend, the guest simply closes the sheet — no server request.
|
||||
|
||||
**Why this priority**: Core interactive feature of the app. Without it, guests cannot communicate attendance, and the attendee list (US-2) has no data.
|
||||
|
||||
@@ -17,30 +31,15 @@ A guest opens an active event page and indicates whether they will attend. If at
|
||||
|
||||
**Acceptance Scenarios**:
|
||||
|
||||
1. **Given** a guest is on an active event page, **When** they select "I'm attending" and enter their name, **Then** the RSVP is submitted to the server, persisted, and the attendee list reflects the new entry.
|
||||
2. **Given** a guest is on an active event page, **When** they select "I'm attending" but leave the name blank, **Then** the form is not submitted and a validation message indicating the name is required is shown.
|
||||
3. **Given** a guest is on an active event page, **When** they select "I'm not attending" without entering a name, **Then** the RSVP is submitted successfully (name is optional for non-attendees).
|
||||
4. **Given** a guest submits an RSVP (attending or not), **When** the submission succeeds, **Then** the guest's RSVP choice, name, event token, event title, and event date are stored in localStorage on this device.
|
||||
5. **Given** a guest submits an RSVP, **When** the submission succeeds, **Then** no account, login, or personal data beyond the optionally entered name is required.
|
||||
1. **Given** a guest is on an active event page, **When** they tap the RSVP CTA, enter their name, and submit, **Then** the RSVP is submitted to the server, persisted, and the attendee count updates.
|
||||
2. **Given** a guest has opened the bottom sheet, **When** they leave the name blank and try to submit, **Then** the form is not submitted and a validation message is shown.
|
||||
3. **Given** a guest has opened the bottom sheet, **When** they close it without submitting, **Then** no server request is made and no state changes.
|
||||
4. **Given** a guest submits an RSVP, **When** the submission succeeds, **Then** the rsvpToken, name, event token, event title, and event date are stored in localStorage on this device.
|
||||
5. **Given** a guest submits an RSVP, **When** the submission succeeds, **Then** no account, login, or personal data beyond the entered name is required.
|
||||
|
||||
---
|
||||
|
||||
### User Story 2 - Re-RSVP from the Same Device (Priority: P2)
|
||||
|
||||
A returning guest on the same device opens an event page where they previously submitted an RSVP. The form pre-fills with their prior choice and name. Re-submitting updates the existing RSVP rather than creating a duplicate.
|
||||
|
||||
**Why this priority**: Prevents duplicate entries and provides a better UX for guests who want to change their mind. Depends on Story 1 populating localStorage.
|
||||
|
||||
**Independent Test**: Can be tested by RSVPing once, then reloading the event page and verifying the form is pre-filled and a second submission updates rather than duplicates the server-side record.
|
||||
|
||||
**Acceptance Scenarios**:
|
||||
|
||||
1. **Given** a guest has previously submitted an RSVP on this device, **When** they open the same event page again, **Then** the RSVP form is pre-filled with their previous choice and name.
|
||||
2. **Given** a guest has a prior RSVP pre-filled, **When** they change their selection and re-submit, **Then** the existing server-side RSVP entry is updated and no duplicate entry is created.
|
||||
|
||||
---
|
||||
|
||||
### User Story 3 - RSVP Blocked on Expired or Cancelled Events (Priority: P2)
|
||||
### User Story 2 - RSVP Blocked on Expired or Cancelled Events (Priority: P2)
|
||||
|
||||
A guest attempts to RSVP to an event that has already expired or has been cancelled. The RSVP form is not shown and the server rejects any submission attempts.
|
||||
|
||||
@@ -58,37 +57,40 @@ A guest attempts to RSVP to an event that has already expired or has been cancel
|
||||
|
||||
### Edge Cases
|
||||
|
||||
- What happens when a guest RSVPs on two different devices? Each device stores its own localStorage entry; the server holds both RSVPs as separate entries (no deduplication across devices — acceptable per design, consistent with the no-account model).
|
||||
- What happens when a guest RSVPs on two different devices? Each device stores its own localStorage entry; the server holds both RSVPs as separate entries (no deduplication across devices — accepted privacy trade-off).
|
||||
- What happens when the server is unreachable during RSVP submission? The submission fails; localStorage is not updated (no optimistic write). The guest sees an error and can retry.
|
||||
- What happens if localStorage is cleared after RSVPing? The form no longer pre-fills and the guest can re-submit; the server will create a new RSVP entry rather than update the old one.
|
||||
- What happens if localStorage is cleared after RSVPing? The sticky bar shows the CTA again (as if no prior RSVP). A new submission creates a duplicate server-side entry — accepted privacy trade-off.
|
||||
- What about spam/abuse on the unprotected RSVP endpoint? Risk is consciously accepted (KISS, privacy-first). No rate limiting, no honeypot, no CAPTCHA. Can be retrofitted in a future spec if real-world abuse occurs.
|
||||
|
||||
## Requirements
|
||||
|
||||
### Functional Requirements
|
||||
|
||||
- **FR-001**: The RSVP form MUST offer exactly two choices: "I'm attending" and "I'm not attending".
|
||||
- **FR-002**: When the guest selects "I'm attending", the name field MUST be required; submission MUST be blocked if the name is blank.
|
||||
- **FR-003**: When the guest selects "I'm not attending", the name field MUST be optional; submission MUST succeed without a name.
|
||||
- **FR-004**: On successful RSVP submission, the server MUST persist the RSVP associated with the event.
|
||||
- **FR-005**: On successful RSVP submission, the client MUST store the guest's RSVP choice and name in localStorage, keyed by event token.
|
||||
- **FR-001**: The RSVP bottom sheet MUST offer an attending flow only: name field (required, max 100 characters) + submit. There is no explicit "not attending" option — the guest simply closes the sheet.
|
||||
- **FR-002**: Submission MUST be blocked if the name is blank or exceeds 100 characters.
|
||||
- **FR-003**: If a prior RSVP for this event exists in localStorage (rsvpToken present), the sticky bottom bar MUST show the guest's RSVP status instead of the initial CTA. Editing the RSVP (name change, withdrawal) is out of scope for this spec.
|
||||
- **FR-004**: On successful attending RSVP submission, the server MUST persist the RSVP associated with the event and return an rsvpToken.
|
||||
- **FR-005**: On successful RSVP submission, the client MUST store the guest's name and the server-returned rsvpToken in localStorage, keyed by event token.
|
||||
- **FR-006**: On successful RSVP submission, the client MUST store the event token, event title, and event date in localStorage (to support the local event overview, US-7).
|
||||
- **FR-007**: If a prior RSVP for this event exists in localStorage, the form MUST pre-fill with the stored choice and name on page load.
|
||||
- **FR-008**: Re-submitting an RSVP from a device that has an existing server-side entry for this event MUST update the existing entry, not create a new one.
|
||||
- **FR-013**: The event page MUST present the event fullscreen with a sticky bottom bar containing the RSVP call-to-action. Tapping the CTA MUST open a bottom sheet with the RSVP form.
|
||||
- **FR-014**: After successful RSVP submission, the bottom sheet MUST close and the sticky bar MUST transition to showing the RSVP status.
|
||||
- **FR-009**: The RSVP form MUST NOT be shown and the server MUST reject RSVP submissions after the event's expiry date has passed.
|
||||
- **FR-010**: The RSVP form MUST NOT be shown and the server MUST reject RSVP submissions if the event has been cancelled [enforcement deferred until US-18 is implemented].
|
||||
- **FR-011**: RSVP submission MUST NOT require an account, login, or any personal data beyond the optionally entered name.
|
||||
- **FR-011**: RSVP submission MUST NOT require an account, login, or any personal data beyond the entered name.
|
||||
- **FR-012**: No personal data or IP address MUST be logged on the server when processing an RSVP.
|
||||
- **FR-015**: The event page MUST show only the attendee count publicly. The full attendee name list is out of scope (see 009-guest-list).
|
||||
|
||||
### Key Entities
|
||||
|
||||
- **RSVP**: Represents a guest's attendance declaration. Attributes: event token reference, attending status (boolean), optional name, creation/update timestamp. The server-side identity key for deduplication is the combination of event token and a device-bound identifier [NEEDS EXPANSION: deduplication mechanism to be defined during implementation].
|
||||
- **RSVP**: Represents a guest's attendance declaration. Attributes: rsvpToken (server-generated UUID, returned to client), event reference, name (required), creation timestamp. Existence of an entry implies attendance — no `attending` boolean. The rsvpToken is returned to the client for future use (editing/withdrawal in a later spec). Duplicates from lost localStorage or different devices are accepted as a privacy trade-off.
|
||||
- **RsvpToken**: A server-generated, event-scoped UUID identifying a single RSVP entry. Modeled as a distinct Java record type (alongside EventToken and OrganizerToken). Stored client-side in localStorage per event.
|
||||
|
||||
## Success Criteria
|
||||
|
||||
### Measurable Outcomes
|
||||
|
||||
- **SC-001**: A guest can submit an RSVP (attending with name, or not attending without name) from the event page without an account.
|
||||
- **SC-002**: Submitting an RSVP from the same device twice results in exactly one server-side RSVP entry for that guest (no duplicates).
|
||||
- **SC-001**: A guest can submit an RSVP (name + submit) from the event page without an account.
|
||||
- **SC-002**: After submitting, the sticky bar shows the guest's RSVP status (not the CTA).
|
||||
- **SC-003**: After submitting an RSVP, the local event overview (US-7) can display the event without a server request (event token, title, and date are in localStorage).
|
||||
- **SC-004**: The RSVP form is not shown on expired events, and direct server submissions for expired events are rejected.
|
||||
- **SC-005**: No name, IP address, or personal data beyond the submitted name is stored or logged by the server in connection with an RSVP.
|
||||
|
||||
Reference in New Issue
Block a user