Happy path (desired)
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.
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.
Block dashboard.
Per event, shows: block name, allocation total, remaining (= total − assigned − checked-in), assigned-but-not-yet-arrived count, checked-in count.
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.
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).
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-panel | Admin → Ticket Blocks | Visible until app ships |
listed-signin-screen | App entry | Sponsor-facing branding |
listed-event-picker | Post-signin | Only sponsor's delegated events |
listed-block-dashboard | Per event | Allocation/remaining/checked-in counts |
listed-add-guest-form | Block dashboard | Name + email + access type |
listed-guest-list | Block dashboard | Per-guest swipe-to-remove (not-checked-in only) |
listed-block-exhausted-banner | 422 BLOCK_EXHAUSTED | "No remaining allocations" |
listed-access-type-not-in-block | 403 ACCESS_TYPE_NOT_IN_BLOCK | Lists allowed types |
listed-cannot-remove-checked-in | 409 CANNOT_REMOVE_CHECKED_IN | "Contact organizer" |
listed-delegation-revoked | 403 DELEGATION_REVOKED | Graceful sign-out |
listed-wrong-app-banner | 403 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