← All stories

BRANCH · ef-022-invitation-transfer-public

Invitation Transfers — Public Side

EF-022 Persona: Transfer recipient (no auth) Stage: Public Roots in: public-event-page

A confirmed guest transfers a ticket by email. The recipient lands on a public transfer-acceptance page, reviews the event, and accepts or declines. The contract is strict: accepting twice does not create two registrations, declined transfers cannot be revived by browser history, and invalid transfer links reveal nothing useful.

Preconditions

Inherits public-event-page with page-resolves. Organizer-side transfer configuration and the sender's authenticated flow are out of scope; this branch starts when the recipient opens a transfer link.

Happy path

  1. Recipient opens /p/:slug/transfer?token=....

    The page validates the transfer token and shows event identity plus the sender name if the sender opted to disclose it.

  2. Recipient accepts.

    The form collects recipient name and email, then atomically moves the ticket entitlement from sender to recipient.

  3. Confirmation renders.

    The recipient sees a confirmed registration; the old token is marked consumed and safe to re-open.

Failure modes

Capacity full mid-fill

Trigger: transfer acceptance starts while capacity is available, but the destination access type fills before submit.

The 409 state explains the event just filled and offers waitlist placement if enabled. The token remains unconsumed unless the waitlist conversion succeeds.

Two-tab idempotency

Trigger: recipient opens the same transfer link twice and accepts both.

The transfer token is consumed once. The second tab shows the existing accepted transfer and does not create a second registration or re-remove sender entitlements.

Network drop during submit

Trigger: accept POST succeeds but response is lost.

Retry with the same Idempotency-Key returns the accepted transfer. Sender and recipient states remain exactly once.

Invalid input rejected without info leak

Trigger: fake transfer token or garbage event slug.

Malformed, unknown, expired-looking, and wrong-event tokens render the same generic transfer-unavailable page.

Source page archived, draft, or deleted

Trigger: transfer link resolves structurally, but the destination event is archived, draft, or deleted.

The transfer page returns one generic 4xx-style unavailable state and does not reveal which event lifecycle state caused it.

Browser back after success

Trigger: recipient accepts, sees confirmation, then presses back.

The accept form does not resubmit. It renders a read-only already-accepted state for the transfer token.

Open Graph tags present

Trigger: recipient shares the transfer URL.

OG tags use the canonical event URL and hero image. The transfer token, sender, and recipient email never appear in metadata.

Bot-fill rate-limited

Trigger: one IP submits transfer accept attempts repeatedly.

The page surfaces 429 or captcha. The token remains unconsumed and the response does not reveal whether the token was valid.

Recipient declines

Trigger: recipient clicks Decline.

The token moves to declined, sender keeps or regains the entitlement per policy, and re-clicking the link shows declined status rather than the accept form.

Sender cancels mid-flow

Trigger: sender revokes the transfer while recipient is typing.

Submit returns a generic unavailable state with no blame and no disclosure of sender action timing.

Recipient email already registered

Trigger: recipient accepts using an email already confirmed for the event.

The server prevents duplicate registration and shows an already-registered transfer resolution. It does not overwrite the existing registration without explicit support.

Stable test attributes

Visibility teeth. Each attribute must be visible when the transfer page is in that state.

data-testWherePurpose
transfer-pageTransfer URLRoot transfer acceptance page
transfer-formInside transfer pageRecipient accept form
transfer-recipient-nameInside formRecipient name input
transfer-recipient-emailInside formRecipient email input
transfer-accept-ctaInside formAccept transfer
transfer-decline-ctaInside transfer pageDecline transfer
transfer-confirmationAccepted stateConfirmed transfer summary
transfer-already-acceptedConsumed token stateExisting accepted transfer
transfer-declinedDeclined stateDeclined transfer summary
transfer-unavailableInvalid/revoked/unavailable stateGeneric transfer unavailable page
transfer-capacity-full409 stateFilled while accepting message
transfer-waitlist-ctaCapacity stateJoin waitlist instead
transfer-rate-limit429 stateRetry-after or captcha message
transfer-already-registeredDuplicate recipient stateRecipient already registered message

Agent test plan

Start from a fixture transfer token and verify accept, decline, consumed token, revoked token, duplicate recipient, capacity race, and anti-probing variants. Sender-side configuration remains a separate organizer branch gap.