← All stories

BRANCH · ef-069-listed-app-ticket-block

Listed App for Ticket Block Users

EF-069 Persona: Sponsor / ticket-block delegate Stage: Pre-event + day-of (native) Roots in: native-app-shell Matrix=Absent — ships as gap probe

A second native app: "Listed" — for sponsors and ticket-block delegates to manage their allocated invitations from a phone. Sponsors see their block's remaining count, add/remove confirmed guests, see who's checked in, without access to the full event admin. Matrix=Absent — story describes the desired contract.

Happy path (desired)

  1. Sponsor receives delegate invitation.

    Organizer assigns the sponsor as a ticket-block delegate (EF-023). Sponsor receives email with deep-link to install Listed app + invitation token.

  2. Sponsor signs in to Listed.

    Open app, paste invitation token (or tap email deep-link). Sign in with email/password (Listed has its own auth, separate from full Voyage staff auth). Lands on event picker showing only events with their ticket-block delegations.

  3. Block dashboard.

    Per event, shows: block name, allocation total, remaining (= total − assigned − checked-in), assigned-but-not-yet-arrived count, checked-in count.

  4. Add a confirmed guest.

    Tap "Add guest" — name + email + access type (constrained to the block's allowed types). Submit decrements remaining. Confirmation email sent to guest with their invite. Audit log row.

  5. Remove a not-yet-arrived guest.

    From the block's guest list, swipe-to-remove a not-yet-checked-in guest. Increments remaining. Cancellation email to guest. Cannot remove already-checked-in (audit-preserving).

  6. Day-of: see who's checked in.

    Block dashboard updates as the event runs — checked-in count increments, remaining decrements, list shows arrival times. Read-only — sponsor can't check anyone in (that's check-in-staff's job).

Failure modes (desired contract)

Parity gap — feature absent

Trigger: matrix=Absent.

Visible "EF-069 Listed app not yet implemented" panel in admin → Ticket Blocks. Without the app, sponsors use the web admin (constrained by ticket-blocks workflow). Until shipped, gap-panel asserts the absence.

Sponsor can only see their own delegations

Trigger: sponsor signs in; event picker shows all events on the tenant.

Server returns events scoped to the signed-in sponsor's active delegations only. Cross-event access (via deep-link or guess) returns 404. Harness: sponsor with delegation on event A, attempt event B URL, 404.

Add-guest exceeds remaining

Trigger: block has remaining=0; sponsor tries to add another.

Submit returns 422 BLOCK_EXHAUSTED with "No remaining allocations." Harness: stub remaining=0, attempt add, 422.

Add-guest with access-type outside block

Trigger: block allows access types [A, B]; sponsor tries to add guest as type C.

Server returns 403 ACCESS_TYPE_NOT_IN_BLOCK. UI surfaces "[Sponsor block name] only allows: VIP, GA." Harness: stub block-allows=[A,B], attempt type C, 403.

Remove-already-checked-in blocked

Trigger: sponsor tries to remove a guest who already checked in.

Server returns 409 CANNOT_REMOVE_CHECKED_IN. UI surfaces "Already checked in — contact organizer to remove." Harness: stub checked-in guest, attempt remove, 409.

Concurrent sponsor + organizer modification

Trigger: sponsor adds guest while organizer simultaneously cancels the block.

Server-side row-level lock on the block. Whichever lands first wins; the other returns 409 CONFLICT with "Block was modified." Harness: 2-actor race, exactly one succeeds, other 409.

Audit log per sponsor action

Trigger: sponsor adds, removes, or signs in.

Each action writes audit row with sponsor_id, block_id, action, timestamp. Organizer's web admin audit view shows sponsor activity. Harness: 3 sponsor actions, 3 audit rows, organizer sees all.

Sponsor invitation token expiry

Trigger: sponsor opens invitation 30+ days after sent (token expires after 30).

Sign-in returns 410 INVITATION_EXPIRED. UI says "Ask organizer to resend." Harness: stub past expires_at, sign-in, 410.

Sponsor permission revoked mid-session

Trigger: organizer revokes sponsor's delegation while sponsor's app session is active.

Within 60s (token cache TTL), next API call returns 403 DELEGATION_REVOKED. UI signs sponsor out gracefully with "Your access was revoked." Harness: revoke + dispatch within 60s, 403, sign-out.

Listed app vs Voyage staff app distinguishability

Trigger: ambiguity — sponsor accidentally installs the staff app, or vice versa.

Different bundle IDs (com.voyage.listed vs com.voyage.app), different App Store listings, different sign-in screens with branding. App Store deep-link in sponsor invitation specifies Listed. Harness: signed-in user with wrong app type, sign-in returns 403 WRONG_APP_FOR_ROLE.

Offline sponsor add-guest

Trigger: sponsor offline; tries to add a guest.

Sponsor-side adds queue locally with optimistic remaining-count decrement. Replay on reconnect. If conflict (block-exhausted server-side), surface error AND rollback the optimistic count. Harness: offline + add, reconnect, success or rollback.

Cross-tenant sponsor invitation forgery

Trigger: forged invitation token for a tenant the sponsor isn't supposed to access.

Token signature is per-tenant. Forged token returns 404 (anti-probing). Harness: forge token, sign-in, 404.

Stable test attributes

listed-gap-panelAdmin → Ticket BlocksVisible until app ships
listed-signin-screenApp entrySponsor-facing branding
listed-event-pickerPost-signinOnly sponsor's delegated events
listed-block-dashboardPer eventAllocation/remaining/checked-in counts
listed-add-guest-formBlock dashboardName + email + access type
listed-guest-listBlock dashboardPer-guest swipe-to-remove (not-checked-in only)
listed-block-exhausted-banner422 BLOCK_EXHAUSTED"No remaining allocations"
listed-access-type-not-in-block403 ACCESS_TYPE_NOT_IN_BLOCKLists allowed types
listed-cannot-remove-checked-in409 CANNOT_REMOVE_CHECKED_IN"Contact organizer"
listed-delegation-revoked403 DELEGATION_REVOKEDGraceful sign-out
listed-wrong-app-banner403 WRONG_APP_FOR_ROLE"Sponsor account — install Listed"

Agent test plan

Probe list
- gap-panel-visible: matrix=Absent, panel visible
- (when shipped) sponsor-event-scoping: only delegated events in picker
- (when shipped) cross-event-404: attempt non-delegated event, 404
- (when shipped) add-exceeds-remaining: stub remaining=0, 422
- (when shipped) wrong-access-type: stub block=[A,B], type C, 403
- (when shipped) remove-checked-in-blocked: 409
- (when shipped) concurrent-modification: 2-actor race, exactly one succeeds
- audit-log-per-action: 3 sponsor actions, 3 audit rows
- (when shipped) token-expiry-410: stub past expires_at, 410
- (when shipped) delegation-revocation-cascades: revoke + 60s, 403
- (when shipped) wrong-app-detected: signed-in role mismatches app type, 403
- (when shipped) offline-add-rollback-on-conflict: offline + add, reconnect with conflict, count rolls back
- cross-tenant-token-forgery-404: forge token, 404