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