← All stories

BRANCH · ef-043-individual-invite

Invite or add individual guests

EF-043Persona: OrganizerRoots in: event-setup

Individual invite is shippable: guest add creates or reuses contact and guest rows at workers/events/src/routes/guests.ts:444, and optional invitation token/event generation starts at workers/events/src/routes/guests.ts:517. This story pins the one-at-a-time add flow and its invite/token boundaries.

Preconditions

Organizer can add guests; fixture includes existing contacts, access types, and ticket blocks.

Happy path / Lifecycle

  1. Open Add individual guest.

    Organizer can search contacts or type a new email, choose access type, ticket block, notes, and send behavior.

  2. Save with send enabled.

    System creates guest row, mints token, and queues invitation email.

  3. Save without send.

    System creates an admin-added guest with no token and no outbound invitation.

Failure modes

Permission denied at the right boundary

Trigger: viewer/support attempts organizer-only operation.

Resolution: the write request returns 403, the editable surface remains closed or read-only, and the response does not leak hidden guest, event, or tenant fields.

Cross-tenant isolation

Trigger: tenant-A user guesses tenant-B resource id.

Resolution: the server returns 404 instead of 403, masks existence, and the UI renders a generic not-found state.

Soft-delete leaves audit trail

Trigger: organizer removes or deactivates the configured object.

Resolution: the row is marked inactive/deleted with actor, timestamp, and prior state preserved in audit.

Archive vs delete distinction

Trigger: organizer chooses between reversible archive and destructive delete.

Resolution: archive stays reversible and copy/export labels it archived; delete requires separate destructive confirmation and changes copy behavior.

Edit lock during publish

Trigger: publish snapshot begins while an edit is open.

Resolution: publish wins; stale save receives a deterministic conflict modal and does not mutate the published snapshot silently.

Audit log row written on every state change

Trigger: organizer saves any state transition.

Resolution: each state mutation writes an audit row with actor, timestamp, entity id, and before/after payload.

Two organizers concurrent

Trigger: two organizers edit the same state from stale versions.

Resolution: the second save gets conflict UI, both sessions refresh to the same final state, and there is no silent overwrite.

Undo window for destructive actions

Trigger: organizer deletes, cancels, or clears the object.

Resolution: a visible undo affordance lasts 10 seconds and restores the exact prior state when used.

Contact reuse vs new contact

Trigger: organizer enters an email already in contacts.

Resolution: autocomplete offers the existing contact; choosing it reuses contact_id, while new email creates a new contact explicitly.

Idempotent invite resend within 60 seconds

Trigger: organizer double-clicks send/resend.

Resolution: duplicate sends within 60 seconds reuse the same operation and do not mint multiple active tokens or emails.

Invite with no token is admin add only

Trigger: organizer adds guest but disables send invitation.

Resolution: guest row is created without an invite token, status copy says admin-added, and no invitation email API is called.

Ticket-block scope enforcement

Trigger: organizer selects a ticket block outside the chosen access type.

Resolution: save is blocked with scoped validation and no guest row is created with mismatched ticket_block_id.

Stable test attributes

Visibility teeth. Each attribute must be effectively visible when active and must match the agent probes.

data-testWherePurpose
individual-invite-drawersurfacedrawer
individual-invite-formsurfaceform
individual-invite-contact-pickersurfacecontact picker
individual-invite-email-inputsurfaceemail input
individual-invite-access-type-pickersurfaceaccess type picker
individual-invite-ticket-block-pickersurfaceticket block picker
individual-invite-notes-inputsurfacenotes input
individual-invite-send-ctasurfacesend cta
individual-invite-archive-invite-ctasurfacearchive invite cta
individual-invite-delete-guest-ctasurfacedelete guest cta
individual-invite-undo-toastsurfaceundo toast
individual-invite-conflict-modalsurfaceconflict modal
individual-invite-validation-errorsurfacevalidation error

Agent test plan

- individual-invite-opens
- individual-invite-saves
- individual-invite-audit-visible
- permission-denied-boundary
- cross-tenant-404
- soft-delete-audit
- archive-delete-distinction
- publish-edit-lock
- audit-row-every-change
- concurrent-organizers-conflict
- destructive-undo-window
- contact-reuse-vs-new-contact
- resend-idempotent-sixty-seconds
- invite-with-no-token-admin-add
- ticket-block-scope-enforcement
- evaluate-individual-invite