Event wizard: how people book (whole course / pick sessions), capacity per session and per-option session mode. Session picker on the public registration page, Manual registration and the kiosk; at the door points to Manual registration. Register, tickets and My Events handle picked sessions. Adds docs/ROADMAP-v2.md and docs/HANDOVER.md so work can carry on from another machine, and a pick-sessions e2e test. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
507 lines
20 KiB
Markdown
507 lines
20 KiB
Markdown
# Hope Events — New Features
|
||
|
||
Planned features for the next round of development. Each section covers what
|
||
the feature does, who uses it, how it fits the existing platform, and the edge
|
||
cases to handle. See [FEATURES.md](./FEATURES.md) for what already exists.
|
||
|
||
---
|
||
|
||
## Contents
|
||
|
||
1. [Coupon codes](#1-coupon-codes)
|
||
2. [Waitlist](#2-waitlist)
|
||
3. [Group tickets](#3-group-tickets)
|
||
4. [Offline scanning](#4-offline-scanning)
|
||
5. [Parental consent (youth events)](#5-parental-consent-youth-events)
|
||
6. [Sponsor a spot](#6-sponsor-a-spot)
|
||
7. [Ticket transfer](#7-ticket-transfer)
|
||
8. [Post-event feedback form](#8-post-event-feedback-form)
|
||
9. [Group / room allocation](#9-group--room-allocation)
|
||
10. [Registration funnel stats](#10-registration-funnel-stats)
|
||
11. [2FA for Admins and Supervisors](#11-2fa-for-admins-and-supervisors)
|
||
12. [Admin "impersonate user"](#12-admin-impersonate-user)
|
||
13. [Suggested build order](#suggested-build-order)
|
||
|
||
---
|
||
|
||
## 1. Coupon codes
|
||
|
||
Discount codes applied at registration. The main use case is **staff
|
||
registering for events** at a reduced or free rate, but codes also work for
|
||
promos, leaders, and volunteers.
|
||
|
||
**Who:** Supervisors create and manage codes. Users enter them at checkout.
|
||
Supervisors can also apply them in Manual registration, At the door and the
|
||
kiosk.
|
||
|
||
### Code settings
|
||
|
||
| Setting | Options |
|
||
|---------|---------|
|
||
| Code | Unique text, case-insensitive (e.g. `STAFF50`) |
|
||
| Discount type | Percentage · Fixed amount · Comp (100% free) |
|
||
| Scope | Whole event · Specific ticket option(s) · Specific variant(s) |
|
||
| Events | One event, several events, or all events |
|
||
| Valid from / until | Optional date window |
|
||
| Max total uses | Optional |
|
||
| Max uses per user | Optional (default 1) |
|
||
| Min quantity | Optional (e.g. 4+ tickets) |
|
||
| Restricted to | Anyone · Specific users · Roles (e.g. Staff and above) · Email domain |
|
||
| Stack with early-bird | Yes (apply on top of early-bird price) / No (best price wins) |
|
||
| Active | On / off toggle |
|
||
|
||
### How it works
|
||
|
||
- The user enters a code on the registration page. It is validated server-side
|
||
and the discount shows as its own line before payment.
|
||
- The discount is stored as a **separate discount line on the registration**,
|
||
with its value **locked at apply time**, the same way early-bird tranches
|
||
are locked. Pricing refresh never re-prices an applied coupon, even if the
|
||
code later expires or is disabled.
|
||
- **Restricted codes** (by user, role or domain) need the user to be logged
|
||
in. Guest registration can't use them. This stops staff codes leaking in
|
||
group chats.
|
||
- **Role-based staff pricing (optional):** a ticket option can also carry a
|
||
"Staff price" that applies automatically for Staff+ users without a code.
|
||
|
||
### Edge cases
|
||
|
||
- **Edit options:** percentage codes recalculate over the new item total.
|
||
Fixed-amount codes stay fixed. Comp codes cover only in-scope options.
|
||
- **Cancellation / refund:** a cancelled registration releases its use count.
|
||
- **Discount can't go below R0.** A comp code on a fully covered registration
|
||
marks it Paid and generates tickets.
|
||
- **Concurrency:** check use limits inside a transaction so the last use
|
||
can't be claimed twice.
|
||
|
||
### Plumbing
|
||
|
||
- Invoices and receipts show the code and discount line.
|
||
- **New report: Discounts.** Uses per code, total value given away, per event.
|
||
- Finance and profit reports subtract discounts from expected revenue.
|
||
- **Audit log:** code created, edited, disabled, and manually applied by staff.
|
||
|
||
### Data model (sketch)
|
||
|
||
```prisma
|
||
model Coupon {
|
||
id String @id @default(cuid())
|
||
code String @unique
|
||
type CouponType // PERCENT | FIXED | COMP
|
||
value Decimal?
|
||
stackEarlyBird Boolean @default(false)
|
||
validFrom DateTime?
|
||
validUntil DateTime?
|
||
maxUses Int?
|
||
maxUsesPerUser Int? @default(1)
|
||
minQuantity Int?
|
||
restrictRoles Role[]
|
||
restrictDomain String?
|
||
active Boolean @default(true)
|
||
events CouponEvent[]
|
||
options CouponOption[]
|
||
allowedUsers CouponUser[]
|
||
redemptions CouponRedemption[]
|
||
createdById String
|
||
createdAt DateTime @default(now())
|
||
}
|
||
|
||
model CouponRedemption {
|
||
id String @id @default(cuid())
|
||
couponId String
|
||
registrationId String
|
||
userId String
|
||
discountAmount Decimal // locked at apply time
|
||
appliedById String? // staff member if applied manually
|
||
createdAt DateTime @default(now())
|
||
releasedAt DateTime? // set on cancellation
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## 2. Waitlist
|
||
|
||
When an event or ticket option is sold out, users can join a waitlist instead
|
||
of hitting a dead end.
|
||
|
||
**Who:** Users join. Supervisors view and manage the queue.
|
||
|
||
### How it works
|
||
|
||
- Sold-out options show a **"Join waitlist"** button instead of "Sold Out".
|
||
- The user picks an option, variant and quantity. Login is required if the
|
||
event requires login. Otherwise a guest account is created as usual.
|
||
- When stock frees up (a cancellation, a refund, or a supervisor raising the
|
||
stock limit), the **next person in line** gets an email/WhatsApp with a
|
||
**claim link**.
|
||
- The claim link **reserves the stock for a set time** (default 24 h, set per
|
||
event). If it isn't used in time, the offer moves to the next person.
|
||
- A claimed offer goes through normal registration and payment, with
|
||
early-bird and coupon rules applied as usual.
|
||
|
||
### Supervisor tools
|
||
|
||
- A waitlist tab per event shows position, person, option, quantity, joined
|
||
date and offer status.
|
||
- Supervisors can manually offer a spot to someone, reorder the queue, or
|
||
remove an entry.
|
||
- Supervisors can toggle the waitlist on or off per event.
|
||
|
||
### Edge cases
|
||
|
||
- Reserved-but-unclaimed stock counts as sold until the offer expires, so the
|
||
public site doesn't oversell.
|
||
- If an offer is for quantity 3 and only 2 free up, offer the 2 with a note,
|
||
or skip to the next person who fits. This is configurable per event.
|
||
- Users can leave the waitlist from their dashboard.
|
||
- Add the waitlist to the Daily summary (count per event).
|
||
|
||
---
|
||
|
||
## 3. Group tickets
|
||
|
||
One payer registers **several named attendees**, each with their own ticket
|
||
and their own form response. For example, a parent registers three kids for
|
||
camp.
|
||
|
||
**This is optional.** The payer can still choose to receive all tickets
|
||
themselves.
|
||
|
||
### How it works
|
||
|
||
- On the registration page, a toggle reads **"Registering for other people?"**
|
||
- For each attendee: name, plus optional email/phone, plus the event form if
|
||
there is one.
|
||
- **Ticket delivery choice per registration:**
|
||
- **Send all tickets to me** (default), so the payer gets every ticket.
|
||
- **Send each person their own ticket**, using their email/WhatsApp. If an
|
||
attendee has no contact details, their ticket falls back to the payer.
|
||
- Tickets are **individual, named tickets** rather than a quantity ticket, so
|
||
each one can be checked in, transferred or allocated to a room/group
|
||
separately.
|
||
- The payer stays the account owner for billing, invoices and payments.
|
||
|
||
### Edge cases
|
||
|
||
- Works alongside the existing quantity tickets, since an event can allow
|
||
either.
|
||
- If an attendee's email/phone matches an existing account, link the ticket
|
||
to that account (with the same duplicate-account protection as today).
|
||
- Attendee forms: each attendee has their own response. The payer can fill
|
||
them in, or each attendee can get a link to fill in their own.
|
||
- Feeds into parental consent (one consent request per minor attendee) and
|
||
group/room allocation.
|
||
|
||
---
|
||
|
||
## 4. Offline scanning
|
||
|
||
Lets the scanner keep working when venue Wi-Fi or mobile data drops.
|
||
|
||
> ⚠️ **Rules: offline mode is per section, and only one device can scan a
|
||
> section offline at a time.**
|
||
> While offline, devices can't see each other's scans. If two devices scanned
|
||
> the same section offline, the same ticket could be redeemed twice. Locking
|
||
> each section to a single device removes that risk without needing conflict
|
||
> resolution.
|
||
|
||
### How it works
|
||
|
||
1. **Before going offline (while online):** a staff member picks an event and
|
||
section and taps **"Enable offline mode"**.
|
||
2. The server **locks that section to this device** and records the device ID
|
||
and the staff member. Other devices see "Offline-locked by [name] on [device]"
|
||
and can't scan that section, online or offline, until it's released.
|
||
3. The device **downloads a snapshot** of every ticket valid for that section
|
||
(ticket ID, holder name, type, quantity remaining) into IndexedDB.
|
||
4. **While offline:** scans validate against the local snapshot and update
|
||
remaining quantities locally. Every scan is queued with a timestamp.
|
||
5. **When back online:** queued scans sync to the server automatically. Any
|
||
conflicts (for example, a ticket transferred or cancelled while offline)
|
||
show in a **sync report** for a supervisor to review.
|
||
6. Staff tap **"Release section"** to end offline mode and unlock the section.
|
||
|
||
### Rules and edge cases
|
||
|
||
- One section can only be locked to one device at a time. Other sections of
|
||
the same event can be locked to other devices.
|
||
- A supervisor can **force-release** a lock if a phone dies or goes missing.
|
||
Unsynced scans on that device are lost unless the device reconnects later,
|
||
in which case they sync and are flagged.
|
||
- **Snapshot age:** show "Ticket list downloaded at 17:42". Tickets bought
|
||
after that time won't be on the device, and the scanner shows "Not found in
|
||
offline list. Check at the door desk."
|
||
- The snapshot is stored only for the lock's duration and cleared on release.
|
||
- Check-in confirmation messages to holders are sent after sync, not
|
||
instantly.
|
||
- **Audit log:** lock, release, force-release, and sync with scan count.
|
||
- Built as a **PWA** (service worker caches the scanner page and assets).
|
||
|
||
---
|
||
|
||
## 5. Parental consent (youth events)
|
||
|
||
For youth events, a parent or guardian must give consent before an attendee
|
||
under 18 can attend. The parent gets an **email and a WhatsApp** with a link
|
||
to review and give consent.
|
||
|
||
### Event setup
|
||
|
||
- New event setting: **"Youth event: requires parental consent"**.
|
||
- Supervisors write the **consent text** (indemnity, activities, photo
|
||
permission, transport, medical treatment authorisation) in the event wizard.
|
||
- Optional extra sections on the consent page: **medical info** (allergies,
|
||
medication, conditions, medical aid), **emergency contact**, and **photo
|
||
consent** (Yes/No, recorded separately).
|
||
|
||
### How it works
|
||
|
||
1. During registration, each attendee gives their **date of birth**. If they
|
||
are under 18, **parent/guardian name, email and phone** are required.
|
||
2. On registration, the parent gets an **email and a WhatsApp** (always both,
|
||
regardless of anyone's notification preference) with a unique, secure
|
||
consent link.
|
||
3. The consent page shows the event details, attendee name and consent text.
|
||
The parent fills in any medical/emergency fields, **types their full name**
|
||
as a signature, ticks "I am the parent/legal guardian", and taps
|
||
**Give consent** (or **Decline**).
|
||
4. The system records the timestamp, IP address, device, typed name and the
|
||
**version of the consent text** they agreed to.
|
||
5. The registrant and the event's notify recipients get a confirmation.
|
||
|
||
### Rules
|
||
|
||
- **Tickets are generated but marked "Consent pending"** until consent is
|
||
given. Scanners show a clear warning and block check-in for that ticket
|
||
(supervisors can override, with an audit log entry).
|
||
- **Reminders:** automatic reminder email/WhatsApp after 2 days, then every
|
||
3 days until the event, or until consent is given or declined.
|
||
- **Declined consent:** notifies the registrant and supervisors. The
|
||
registration stays but can't be checked in. A supervisor handles the refund.
|
||
- **Consent links expire** at the event's end date, and can be resent from
|
||
the registration detail screen.
|
||
- If the consent text is edited after some parents have consented, they keep
|
||
their record of the old version. Supervisors can choose to re-request
|
||
consent.
|
||
|
||
### Supervisor tools
|
||
|
||
- **Consent status column** on registrations and At the door (Pending /
|
||
Given / Declined).
|
||
- **New reports:** Consent status per event, and **Medical & emergency list**
|
||
(printable, for camp leaders). Access to the medical list is restricted to
|
||
Supervisor+ and logged.
|
||
- Bulk "Resend consent request" to everyone still pending.
|
||
|
||
### Privacy
|
||
|
||
Under POPIA, processing a child's personal information needs the consent of a
|
||
competent person (parent/guardian). Medical data is special personal
|
||
information. Store it encrypted, restrict access, include it in the existing
|
||
anonymise/delete flows, and delete it a set period after the event.
|
||
|
||
---
|
||
|
||
## 6. Sponsor a spot
|
||
|
||
A public donation flow linked to a specific event, so people can fund a spot
|
||
for someone who can't afford it (bursaries).
|
||
|
||
### How it works
|
||
|
||
- A new event setting: **"Allow sponsorships"**. This adds a **"Sponsor a
|
||
spot"** button on the event page.
|
||
- Sponsors choose: **one full spot** (the main ticket price), **multiple
|
||
spots**, or **a custom amount**. Payment goes through Yoco as usual.
|
||
- A sponsor can be anonymous, or add a name and message.
|
||
- The money is recorded as a **donation tagged to the event**, and lands in a
|
||
**sponsorship pool** for that event.
|
||
- Supervisors assign pool funds to registrations using the existing donation
|
||
assign/unassign flow, fully or partly.
|
||
- An optional public progress bar shows "12 of 20 spots sponsored".
|
||
|
||
### Plumbing
|
||
|
||
- Sponsors get a receipt. They can optionally get a thank-you after the event
|
||
through Automations.
|
||
- The Donations breakdown report gains a sponsorship pool column (received,
|
||
assigned, remaining) per event.
|
||
- Recipients' identities are never shown to sponsors.
|
||
|
||
---
|
||
|
||
## 7. Ticket transfer
|
||
|
||
Users can hand their ticket to someone else before the event.
|
||
|
||
### How it works
|
||
|
||
- From My Events, a user taps **Transfer** on a ticket, and enters the new
|
||
holder's name and email/phone.
|
||
- The new holder gets the ticket by email/WhatsApp. If they don't have an
|
||
account, an inactive one is created with an activation link (same as guest
|
||
registration).
|
||
- The **old QR code is invalidated** and a new one is issued, so screenshots
|
||
of the old ticket won't scan.
|
||
- The original payer stays responsible for the registration (billing doesn't
|
||
move).
|
||
|
||
### Rules
|
||
|
||
- Allowed only before the event starts, and only for unredeemed tickets
|
||
(partially redeemed quantity tickets can't be transferred).
|
||
- Can be disabled per event. Supervisors can always transfer.
|
||
- If the event needs a form or parental consent, the new holder must complete
|
||
their own.
|
||
- **Audit log:** transfer from, to, by whom and when. Offline scanning sync
|
||
flags any scan of a transferred ticket's old code.
|
||
|
||
---
|
||
|
||
## 8. Post-event feedback form
|
||
|
||
An automatic survey sent after the event.
|
||
|
||
### How it works
|
||
|
||
- In the event wizard, a new **Feedback** step reuses the form builder, plus
|
||
new field types: **Rating (1–5 stars)** and **Multiple choice**.
|
||
- Supervisors pick a **send time** (e.g. the morning after the event ends).
|
||
It's sent by the existing scheduled job to everyone who **checked in**, or
|
||
to all registrants, as chosen.
|
||
- Responses can be anonymous or named (per event setting).
|
||
- One automatic reminder after 3 days for anyone who hasn't responded.
|
||
|
||
### Reporting
|
||
|
||
- **New report: Feedback summary.** Response rate, average rating per
|
||
question, multiple-choice breakdown (bar charts), and a list of text
|
||
answers. Exports to PDF/Excel like other reports.
|
||
- Ratings can be compared across events (e.g. Camp 2025 vs Camp 2026).
|
||
|
||
---
|
||
|
||
## 9. Group / room allocation
|
||
|
||
Assign attendees to cabins, rooms, small groups, teams or buses. Built for
|
||
camps and lockdowns.
|
||
|
||
### How it works
|
||
|
||
- Per event, supervisors create **allocation sets**, e.g. "Rooms", "Small
|
||
groups", "Buses". An event can have several sets.
|
||
- Each set has **groups** with an optional **capacity**, an optional
|
||
**gender restriction** and an optional **leader** (a staff user).
|
||
- **Drag-and-drop** board: unassigned attendees on the left, groups on the
|
||
right, with capacity counters.
|
||
- **Auto-allocate:** fill groups evenly, respecting capacity and gender, and
|
||
optionally keeping registrations together (siblings) or splitting them
|
||
(friends).
|
||
- Works per attendee, so it relies on group tickets (named attendees).
|
||
|
||
### Outputs
|
||
|
||
- Printable lists per group (with leader), and a master list.
|
||
- Optionally show the group/room on the attendee's ticket and in My Events.
|
||
- Group leaders (staff) can see their group's list, including medical
|
||
info if they're allowed to.
|
||
- Bulk message by group ("Bus 2 leaves at 07:30").
|
||
|
||
---
|
||
|
||
## 10. Registration funnel stats
|
||
|
||
Shows where people drop off between seeing an event and paying.
|
||
|
||
### Funnel stages (per event)
|
||
|
||
1. **Event page views** (unique visitors)
|
||
2. **Registration started** (opened the registration page)
|
||
3. **Registration submitted**
|
||
4. **Paid** (fully paid or free)
|
||
|
||
### How it works
|
||
|
||
- Lightweight first-party tracking (no third-party analytics). Log page views
|
||
and stage events with an anonymous session ID. No personal data until the
|
||
user logs in or registers.
|
||
- **New report: Registration funnel.** Count and conversion % at each stage,
|
||
over a date range, with a daily trend chart.
|
||
- Optional breakdowns: by traffic source (UTM tags or short link alias, e.g.
|
||
`/camp-2025` vs direct), by device (mobile/desktop), and by ticket option.
|
||
- A dashboard tile shows conversion % for active events.
|
||
|
||
### Privacy
|
||
|
||
Respect cookie consent. Aggregate raw page-view data after 90 days.
|
||
|
||
---
|
||
|
||
## 11. 2FA for Admins and Supervisors
|
||
|
||
Two-factor authentication, **required** for Admin and Supervisor accounts.
|
||
|
||
### How it works
|
||
|
||
- **Method:** authenticator app (TOTP, e.g. Google Authenticator, Microsoft
|
||
Authenticator). WhatsApp code can be offered as a backup method since WAWP
|
||
is already set up.
|
||
- **Enrolment:** on the next login after being given the Supervisor/Admin
|
||
role, the user must set up 2FA by scanning a QR code and confirming a code.
|
||
- **Recovery codes:** 10 one-time codes shown once at setup, downloadable.
|
||
- **Remember this device** for 30 days (optional, per login).
|
||
- Optional for Staff and User roles, from Profile & Security.
|
||
|
||
### Admin controls
|
||
|
||
- Admins can **reset another user's 2FA** (e.g. a lost phone). This revokes
|
||
their sessions and forces re-enrolment.
|
||
- Security event log entries: 2FA enabled, disabled, reset, recovery code
|
||
used, failed code attempts.
|
||
- Failed 2FA attempts count toward the existing login lockout.
|
||
- The setup wizard enrols the first admin in 2FA.
|
||
|
||
---
|
||
|
||
## 12. Admin "impersonate user"
|
||
|
||
Lets an Admin see the site exactly as a specific user sees it, to debug
|
||
problems ("my ticket isn't showing").
|
||
|
||
### How it works
|
||
|
||
- From Manage users, an Admin taps **Impersonate** on a user and must enter a
|
||
**reason** (required).
|
||
- A short-lived session is created (max 30 minutes) that acts as the user.
|
||
- A **bright banner on every page**: "Viewing as [Name]. [End impersonation]".
|
||
|
||
### Safety rules
|
||
|
||
- **Read-only by default.** Payments, refunds, cancellations, password/email
|
||
changes, deleting data, giving consent and sending messages are blocked
|
||
while impersonating.
|
||
- Can't impersonate another Admin.
|
||
- Requires 2FA to be active on the admin's account.
|
||
- **Audit log:** start, end, reason, and every page/action during the
|
||
session.
|
||
- Optional: notify the user that an admin viewed their account (an
|
||
organisation-level setting).
|
||
|
||
---
|
||
|
||
## Suggested build order
|
||
|
||
| Phase | Features | Why |
|
||
|-------|----------|-----|
|
||
| 1 | 2FA, Coupon codes | Security first. Coupons are self-contained and give an immediate win for staff. |
|
||
| 2 | Group tickets | Foundation for consent, transfer and room allocation (named attendees). |
|
||
| 3 | Parental consent, Waitlist | The biggest wins for youth events. |
|
||
| 4 | Offline scanning, Ticket transfer | Door-day reliability. Transfer must exist before offline sync handles it. |
|
||
| 5 | Group/room allocation, Sponsor a spot | Camp-specific and bursary support. |
|
||
| 6 | Feedback form, Funnel stats, Impersonate user | Insights and support tooling. |
|
||
|
||
Each phase should add matching help content (`frontend/src/content/help/`),
|
||
audit log entries, a CHANGELOG entry, and updates to FEATURES.md and
|
||
SETTINGS.md.
|