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,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.