# Send an invitation to one guest, from the guest list

## Why

Sending an invitation to one named guest is the most frequent thing a host does, and today
the admin has no way to do it. The bulk send screen mails an entire audience; the guest
drawer offers a link to copy and a QR to photograph, both of which push the actual sending
into WhatsApp or a phone's mail app. So the one action the product is named for — *invite
this person* — happens outside the product, and nothing records that it happened.

The drawer that would host that action is also confused about what it is. It opens from a
name that does not look like a control, its QR block occupies more vertical space than
everything else combined for a use case (printing) that belongs to the per-event QR screen,
and its three sections are visually undifferentiated, so the link, the RSVP override and the
timeline read as one undivided wall.

## What Changes

**Guest list**

- A **Send invitation** action on every guest row, in the actions column beside Edit and
  Remove. It opens the guest detail modal.
- **BREAKING (UI):** the guest name stops being a button. It renders as text; the modal is
  reached through the explicit action. One affordance, named for what it does.

**Guest detail modal**

- **BREAKING (UI):** the **Show QR** control and its download block are removed from this
  modal. Per-guest QR artwork stays reachable from the per-event QR screen, which is where
  printing is actually planned.
- The link controls gain the heading **Send via copying the link**.
- A new section, **Send via messaging**, holds an editable composed message and a **Send via
  email** button. It is seeded from the server with:
  - header — `Dear {guest_name}`
  - body — the event's invitation message for this guest's type (single vs family/multiple),
    resolved event → wedding → built-in default, followed by the guest's invitation link
  - footer — `Regards {host_name}`
  - subject — editable, defaulted from the event title
  Every part is editable before sending, and edits apply to that one send only.
- Each section is a distinct visual block — its own surface, border and heading — so the
  three jobs in this modal (copy a link, compose a message, correct an RSVP) are told apart
  at a glance.

**Sending**

- New endpoints: `GET /api/admin/invitations/{id}/message` composes the default message
  server-side, and `POST /api/admin/invitations/{id}/send` sends an edited one.
- A manual send is a real `message_job` — same queue, same audit trail, same delivery
  webhooks as every other send — but it carries its own composed subject and body rather
  than rendering a template, and it is dispatched inside the request so the admin sees the
  true outcome rather than "queued".
- Manual sends are **repeatable**: each gets its own idempotency key, and the panel shows
  when this invitation was last emailed so a duplicate is visible before it is sent.
- Quiet hours (22:00–08:00 Asia/Dhaka) are **warned about, then overridden** on explicit
  confirmation. Automated waves keep deferring; a human clicking Send twice does not.
- Suppression is **not** overridable: `do_not_contact`, `email_invalid` and a missing address
  each disable the button with the reason stated.
- Delivery uses the configured provider unchanged. No provider is configured yet, so with
  `DRY_RUN` the send is logged and reported honestly as a rehearsal; when the key is missing
  in production the endpoint fails with a message naming the missing setting rather than a
  provider stack trace. Wiring a real Postmark/Resend key stays a later, separate change.
- `Reply-To` is set to a new wedding-level host email so guest replies reach the couple; the
  `From` address stays the platform sender, because that is the identity SPF and DKIM
  authorise.

## Capabilities

### New Capabilities

None — this extends existing surfaces rather than introducing a new one.

### Modified Capabilities

- `messaging`: adds per-invitation manual send — server-composed defaults, admin-edited
  subject and body persisted on the job, per-send idempotency, quiet-hours override on
  explicit confirmation, non-overridable suppression, host `Reply-To`.
- `guest-management`: the guest row gains an explicit Send-invitation action and loses the
  name-as-button; the guest detail modal gains the compose-and-send section, section
  headings and section separation.
- `qr-codes`: per-guest QR is no longer offered from the guest detail modal; the per-event
  QR screen remains the single place QR artwork is produced.

## Impact

**API**

- `api/app/routers/admin_guests.py` — two new routes under `/admin/invitations/{id}`.
- `api/app/services/messaging.py` — compose helper; `process_job` prefers a job's stored
  subject/body over template rendering; quiet-hours deferral becomes conditional.
- `api/app/models/messaging.py` + new Alembic revision — `message_job.subject`,
  `message_job.body_text`, `message_job.override_quiet_hours`, `message_job.sent_by_admin_id`.
- `api/app/models/wedding.py` + same revision — `wedding.host_email`.
- `api/app/services/providers.py` — `SendRequest` carries `reply_to`.
- `api/app/routers/admin_wedding.py` — host email in the wedding settings payload.
- Policy: reuses `Action.SEND_MESSAGES`; no new action, no matrix change.

**Web**

- `web/components/admin/GuestsView.tsx` — row action, name de-linked.
- `web/components/admin/GuestDrawer.tsx` — QR block removed, sections restructured and
  headed, new compose panel component.
- `web/components/admin/InviteCompose.tsx` (new).
- `web/lib/api/browser.ts`, `web/lib/api/schema.d.ts` — regenerated client (`make client`).
- `canGenerateQr` stops being threaded into the guest drawer.

**Docs**

- `docs/USER-GUIDE.md` — the invite-one-guest path replaces "copy the link and paste it".
