## Purpose

Getting guests into the system and keeping them correct: manual add/edit, bulk CSV import with mapping and per-row validation, event tagging, deduplication, and soft delete.

> **Amended by `add-event-invitation-card` (design D11).** A guest record now belongs to
> exactly one event. Read every requirement below with that in mind:
>
> * "per-event invitations" is one invitation, to the guest's own event. There is no
>   `events` list on the write model and no `events` column read from an import.
> * Uniqueness of phone and email is **per event**, not global. The same person on two guest
>   lists is two records, and that is correct rather than a duplicate.
> * Add, list, edit and import are addressed by event. The destination comes from the route,
>   so a request body or a spreadsheet column cannot contradict it.
>
> The authoritative statements are in `add-event-invitation-card/specs/event-management`.

## ADDED Requirements

### Requirement: Manual guest add and edit (FR-4.8, P0)
Admins SHALL add and edit guests with: full name, email, phone, WhatsApp number (defaults to phone), side (bride/groom/common), group tags, preferred locale, preferred channel, and per-event invitations with `max_guests`. Phone numbers are normalised to E.164 (+880…) on save; a guest with neither phone nor email is rejected.

#### Scenario: Add guest with local phone
- **WHEN** an admin adds a guest with phone `01712345678`
- **THEN** the guest is stored with `+8801712345678`

#### Scenario: No contact info
- **WHEN** an admin submits a guest with neither phone nor email
- **THEN** the save is rejected with a clear reason

### Requirement: Bulk CSV import (FR-4.8, §14.2, P0)
The system SHALL import guests from CSV with: column mapping UI, a preview of the first 10 rows, duplicate detection, and a per-row error report. The template columns are `full_name,email,phone,whatsapp,side,group_tags,events,max_guests,locale,preferred_channel`. Rules: phone accepts local or E.164 and is normalised; `events` is a comma-separated subset of `mehedi,marriage,walima`; blank whatsapp defaults to phone; rows with neither phone nor email are rejected with a reason. Import of a valid 800-row file completes without timeout.

#### Scenario: Import with mixed validity
- **WHEN** an 800-row CSV contains 12 invalid rows
- **THEN** 788 guests are imported, and a per-row error report identifies each rejected row and why

#### Scenario: Events column creates invitations
- **WHEN** a row lists `events` = `marriage,walima` with `max_guests` 4
- **THEN** the guest gets two invitations, each with max_guests 4

#### Scenario: Re-import is idempotent
- **WHEN** the same CSV is imported twice
- **THEN** no duplicate guests are created — matches on `(wedding_id, phone_e164)` or `(wedding_id, lower(email))` update or skip instead (§4.2)

### Requirement: Invitation issuance and tokens (§4.2, §7.6, P0)
Each guest×event invitation SHALL carry a unique 22-character URL-safe crypto-random token (128-bit, not guessable or sequential) and a unique 6-character human-typeable short code. Tokens are treated as bearer secrets: never logged in full, and stripped from Referer via `Referrer-Policy: no-referrer`.

#### Scenario: Token generation
- **WHEN** an invitation is created
- **THEN** it has a unique 22-char URL-safe token and a unique 6-char short code

### Requirement: Soft delete (§4.2, P0)
Guest deletion SHALL be a soft delete (`is_deleted`), permitted only for roles with delete capability, and excluded from counts, sends, and exports by default.

#### Scenario: Deleted guest excluded
- **WHEN** a guest is deleted
- **THEN** they no longer appear in dashboard counts, send audiences, or default exports, but their history remains for audit
