Files
joshuaandClaude Opus 5.5 a1ecbb9617 POPIA toolkit part 3: retention, breach register, readiness
- Retention policies (Admin -> Privacy -> Retention) applied by a nightly
  `retention` job: inactive accounts anonymised after 5 years with a
  30-day warning (on by default; never staff roles or people with an
  upcoming registration), form answers, login history, payer details in
  payment notifications, audit/access logs
- User.lastLoginAt (set on login, backfilled) and inactivityWarnedAt
- Breach register (SecurityBreach) with drafts for the people affected
  and the Information Regulator
- Readiness checklist with a score
- Children's/medical flag in the access log; {retention} placeholder in
  the privacy notice
- Unit tests (retention.test.js), e2e (privacy-retention.spec.ts); docs

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012mt6TCv85T9VKRq3cV7rXq
2026-10-23 20:25:58 +02:00

698 lines
40 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Hope Events — Feature Overview
A complete map of what the platform does, grouped by who uses it. For setup and
deployment see [README.md](./README.md); for the API see
[backend/API_DOCUMENTATION.md](./backend/API_DOCUMENTATION.md); for setting
defaults see [SETTINGS.md](./SETTINGS.md); for history see
[CHANGELOG.md](./CHANGELOG.md).
**Stack:** Next.js (React, Tailwind, shadcn/ui) frontend · Express + Prisma +
PostgreSQL backend · Yoco for card payments · SMTP for email · WAWP for
WhatsApp · PM2 for process management · optional Sentry for error monitoring.
---
## Contents
1. [Roles](#roles)
2. [Public site](#public-site)
3. [Events](#events)
4. [Registration](#registration)
5. [Payments, donations & refunds](#payments-donations--refunds)
6. [Tickets & check-in](#tickets--check-in)
7. [Attendee forms](#attendee-forms)
8. [Messaging & notifications](#messaging--notifications)
9. [User dashboard (My Events)](#user-dashboard-my-events)
10. [Staff tools](#staff-tools)
11. [Supervisor tools](#supervisor-tools)
12. [Admin tools](#admin-tools)
13. [Cashup & event costs](#cashup--event-costs)
14. [Reports](#reports)
15. [Site settings & setup wizard](#site-settings--setup-wizard)
16. [Accounts & security](#accounts--security)
17. [Privacy (POPIA)](#privacy-popia)
18. [Help system](#help-system)
19. [Background jobs](#background-jobs)
20. [Operations](#operations)
21. [Page map](#page-map)
---
## Roles
Four roles, each including everything the roles below it can do:
| Role | Can do |
|------|--------|
| **User** | Browse events, register, pay, donate, view tickets, fill in forms, manage their own profile |
| **Staff** | Scan tickets, view/print event tickets, view registrations and form responses |
| **Supervisor** | Create and manage events, record payments/refunds, assign donations, at-the-door desk, manual registration, bulk email/WhatsApp, reports, site banner, self-service kiosk |
| **Admin** | Manage users and roles, all registrations, event costs and cashup (close/reopen events), audit log, site settings, WhatsApp connection, backups |
---
## Public site
- **Home page** — hero section with the organisation's name and branding, upcoming
and in-progress events, and a "Join Us" button (becomes "My Dashboard" when
logged in).
- **Events list** (`/events`) — event cards with picture, dates, location,
price, and a Register / Sold Out / Contact us button. A diagonal "Early bird"
ribbon in the top-right corner while an early-bird tier is running and
registration is open.
- **Event detail** (`/events/[id]`) — description, ticket options with
variant and early-bird pricing, low-stock and sold-out badges, downloadable
attachments, embedded Google Map with "Directions" link, and an
**Add to calendar** (`.ics`) button.
- **Short links** — events can have an alias (e.g. `/camp-2025`) that redirects
to the event. Only plausible aliases hit the database, so bot probes are
rejected cheaply.
- **Contact page** (`/contact`) — organisation email, phone, address and map.
- **Legal pages** — Terms of Use and Privacy Policy, filled from Site Settings.
- **Site banner** — a site-wide announcement bar set by supervisors.
- **Lockdown rules** (`/lockdown-rules`) — static rules page for the youth
lockdown event.
- **SEO** — `sitemap.xml` (every public page and event), `robots.txt`, and
Open Graph/Twitter link-preview metadata.
- **Theming** — brand colours, logo and favicon come from Site Settings.
---
## Events
Created and edited by supervisors through a 9-step wizard: **Basic Details →
Items & Pricing → Sections → Form → Visibility → Notifications → Attachments →
Feedback → Attendance**.
- **Basic details** — title, description, start/end dates, registration
deadline, picture (upload or URL), location (defaults to the org's address),
contact person.
- **Contact-only events** — for things like baptisms: shown publicly with a
"Contact us" popup instead of registration, with no tickets.
- **Multi-day events (sessions)** — one event on several dates, e.g. every
Wednesday 18:30–20:30 for 5 weeks. A repeat pattern (daily/weekly, every N,
chosen weekdays, a number of sessions or an end date) creates the sessions,
which can then be edited one by one (label, times, location), added to, or
cancelled (kept for history; one with check-ins can't be deleted). People
register and pay once and get one ticket. The event page shows the schedule
with the next session highlighted; cards show "5 sessions · Wed 4 Nov – Wed
2 Dec" and stay listed until the last session. Calendar files get one entry
per session. Saving moved or cancelled sessions offers to message attendees.
- **Pick sessions** (multi-day) — instead of the whole course, people can choose
which sessions to come to (day passes, drop-in classes). Tickets set to "Pick
sessions" are priced and bought per session — one line and one ticket per session,
bookable until the session starts (staff: until it ends) — alongside optional
whole-course tickets. Sessions can have a capacity (picked places plus whole-course
tickets); a per-session ticket's stock limit is places per session. Picked tickets
only scan on their own session; reminders, the register and certificates follow
the sessions each person booked. "All sessions" tickets on such an event also show
the sessions: people tick the ones they'll come to and pay the ticket's one price; the
ticket scans, reminds and counts towards capacity for those sessions only. The waitlist
works per session (a full session shows "Join waitlist"). Bookable from the registration page, Manual registration,
At the door (today's session is preselected) and the kiosk. Not available on youth
events.
- **Late joining** (multi-day) — registration closes when the first session
starts, or later (until session N) with late joining on. Late joiners pay
full price, a pro-rata price for the sessions left (rounded to the nearest
R10), or a fixed late price — main tickets only.
- **Completion certificates** — optional per event: four designs (Classic,
Modern, Church, Kids), custom wording with placeholders, signature name/role/
image, and a completion rule (every session, at least N sessions, or N%).
Issued automatically when the event ends; each has a code and QR code checked
on a public verify page.
- **Ticket options** — multiple options per event (e.g. Adult, Child, Dinner,
T-shirt), each with a price and optional **stock limit**. One option is marked
as the main ticket, which is what gets checked in at the door.
- **Variants** — sub-types of an option (sizes, colours, Adult/Child/VIP), each
with an optional price override and its own stock limit.
- **Early-bird tiers** — deadline- and/or stock-limited cheaper prices per
option or variant. Prices are locked in per purchase ("tranches"), so buying
more later doesn't re-price what was already bought.
- **Sections** — named scan points (e.g. "Main hall", "Dinner") limited to
certain ticket options, so a scanner at that point only accepts those tickets.
- **Visibility** — active/inactive, hidden (reachable only by link), scheduled
go-live date, require login vs. allow guest registration, or no registration
at all.
- **Attachments** — downloadable files (maps, schedules, waivers) shown on the
event page.
- **Notify recipients** — choose which staff get registration/payment alerts and
the daily summary for the event.
- **Sold-out handling** — an event is sold out when every stock-limited option
is exhausted. "X remaining" badges appear at a threshold that scales with
capacity.
---
## Registration
- **Online registration** (`/register/[eventId]`) — pick options, variants and
quantities; see early-bird prices and stock warnings; fill in the event form.
- **Guest registration** — when an event doesn't require login, an inactive
account is created and an activation link is sent by email or WhatsApp.
- **Statuses** — Pending, Confirmed, Partially Paid, Paid, Cancelled.
- **Edit options** — users can add or change items on their registration
(not for past or closed events).
- **Self-cancellation** — allowed only before any payment; admins can cancel
any time.
- **Pricing refresh** — when an unpaid registration is viewed or paid, expired
early-bird prices are re-evaluated, and the user is warned if the total
changed.
- **Manual registration** (supervisor) — register someone on their behalf,
creating an account if needed. Includes a "Record Payment" tab.
- **Duplicate-account protection** — if an email and phone belong to two
different accounts, the registration is blocked instead of merging them.
- **Group tickets** — on events with "Named attendees" turned on, someone booking
several places can name each person (with optional email/phone). Each named person
gets their own ticket and their own attendee form; unnamed places share one ticket.
Tickets go all to the payer, or each to the person (falling back to the payer).
Attendees matching an existing account see their ticket in My Events. Names can be
added or changed later from My Events, and attendees can be sent a link to fill in
their own form. Available on the registration page, manual registration, At the
door and the kiosk.
- **Waitlist** — on events with a waitlist, sold-out tickets show "Join waitlist"
(on the event page, the register page and the event list). When places free up — a
cancellation, a smaller booking or a higher stock limit — the next person is
emailed/WhatsApped a claim link and the places are held for them (24 hours by
default) so nobody else can buy them. Unclaimed offers expire and move on; people
who asked for more than came free can be offered what's free or skipped. Claiming
works without logging in. People can leave from My Events. On pick-sessions events
people wait for a particular session; offers never run past the session's start.
- **Parental consent (youth events)** — on events that require it, every attendee
gives a date of birth; for anyone under 18, a parent or guardian gets an email and
a WhatsApp with a consent link. They read the event's consent text, optionally give
medical info, an emergency contact and photo consent, type their name to sign, and
give or decline. Tickets are issued but can't be checked in until consent is given
(supervisors can override, audited). Reminders go after 2 days, then every 3 days.
Medical details are encrypted and deleted a set number of days after the event.
- **Coupon codes** — percentage, fixed-amount or free (comp) codes, entered under
"Have a code?" on the registration page, in My Events (for unpaid registrations),
manual registration, At the door and the kiosk. Codes can be limited to events,
ticket options or variants, dates, total and per-person uses, a minimum quantity,
and specific people, roles or an email domain (restricted codes need a login).
Early-bird prices either stack or the better price wins. The discount is locked in
when applied and shows on invoices, receipts, emails and the Yoco checkout;
cancelling frees the use. Managed from Supervisor → Coupon codes.
- **Staff price** — a ticket option or variant can have a staff price that Staff and
above get automatically, without a code.
---
## Payments, donations & refunds
- **Online card payments via Yoco** — checkout for the full outstanding balance,
confirmed by a signed webhook. Keys and the webhook are set up from
Site Settings → Payments, where providers are switched on or off and put in order
(the first one is the default). Online payments are at least R15; payments are
recorded at the full amount and provider fees are dealt with at cashup.
- **PayFast** — the second online provider (card, instant EFT…), with sandbox and live
modes. When several providers are on, payers choose on the "Pay online" page; a
payment of a different amount from the checkout is held for review instead of being
recorded. When no provider is on (or the amount is under R15), nothing offers to pay
online.
- **Payment status checks** — if a provider's notification never arrives, Payments →
Reconcile → "Waiting for payment" checks with the provider (PayFast; Yoco's API can't be
asked) and records what it finds. A job checks recent PayFast checkouts every 15 minutes.
- **Payment links** — supervisors can generate and send a Yoco link for any
amount (including partial).
- **Manual payments** — cash, card, EFT, voucher or other, recorded by staff.
Each payment records **who recorded it** as well as who it's for.
- **Refunds** — negative payments against an original payment, netted against
the original method.
- **Impersonate user** (Admin → Manage users) — an admin with 2FA can view the site as a
user, to debug problems.
- It needs a reason, lasts at most 30 minutes, and shows a bright "Viewing as" banner
on every page.
- It is strictly read-only. A server-side guard refuses every write made with the
impersonation token before it reaches any route, and pages don't even refresh
prices while viewing.
- Admins can't be impersonated.
- The start, end, reason and every page and API call are in the audit log, with each
session's trail.
- Optional email to the user (`notify_user_on_impersonation`).
- **Registration funnel stats** — first-party and consent-based, with no third-party analytics.
- A cookie notice asks once. Only visitors who allow it get a random visit id.
- We record event page views and registration starts. Submits and payments come
from the registration itself.
- Each record holds only the stage, the traffic source (utm_source, or the short-link
alias, which short links now pass as `?src=`) and the device type.
- The Registration funnel report shows counts and conversion per stage over a date
range, a daily trend, and breakdowns by source, device and ticket option.
- A "Conversion (past month)" tile on the supervisor/admin dashboards covers live
events.
- Raw rows are rolled up into daily totals after 90 days.
- **Post-event feedback** — a Feedback step in the event wizard.
- Questions use the form builder, with new **star rating (1–5)** and **multiple
choice** types (also available on attendee forms).
- The form goes out at a chosen time to everyone who checked in, or to everyone
registered, each with a personal link (`/feedback/<token>`). One reminder goes
3 days later to anyone who hasn't answered.
- Answers can be anonymous or named. "Send now" is available, and the questions
lock once people have answered.
- **Sponsor a spot** — per-event setting. A "Sponsor a spot" button on the event page
lets anyone, including guests, pay for one or more spots (at the main ticket price)
or any amount through Yoco. They can give anonymously or add a name and a message.
The money is a donation tagged to the event and goes into its sponsorship pool, which
supervisors assign to registrations with the usual donation flow.
- An optional public progress bar ("12 of 20 spots sponsored") with an optional goal.
- Sponsors get a thank-you receipt, and an Automations thank-you can go to all of an
event's sponsors.
- The Donations breakdown report shows each pool: received, assigned and remaining.
- Who is helped is never shown.
- **Donations** — users can donate from their dashboard (`/dashboard/user/donate`).
Supervisors can **assign** a donation (fully or partly) to someone's
registration and **unassign** it later. Assignments are kept as separate
records, so the original donation never changes.
- **Online payment reconciliation** — provider (Yoco) transactions can be matched to a
registration, recorded as a donation (optionally choosing the donor), or
ignored.
- **Invoices & receipts** — branded PDFs: an invoice (itemised, early-bird
discount, balance due, pay-now link and QR code) and a payment receipt. Sent
automatically and re-sendable on demand by email or WhatsApp.
- **Method normalisation** — Apple Pay / Google Pay / checkout payments count as
"Card" in reports and user views.
---
## Tickets & check-in
- **Automatic tickets** — QR-coded tickets are generated when a registration is
fully paid (or free), and sent as a PDF by email and/or WhatsApp.
- **Quantity tickets** — one ticket can cover several people and be redeemed
partially (e.g. check in 2 of 3).
- **Named tickets** — group bookings can give each person their own ticket, shown
with their name on the PDF, the scanner and the staff ticket list.
- **QR scanner** — works in any mobile browser. A preview checks the ticket is
for the selected event/section, then a confirmation screen shows holder, type
and remaining quantity.
- **Check-in confirmation** — holders get an email/WhatsApp saying how many were
checked in and how many remain.
- **Per-session check-in** — multi-day tickets are checked in once per session.
The scanner uses today's session (or one picked from a list). Check-in is open
the whole day of a session, or only from 2 hours before it until it ends, per
event. Optional reminders (1 h / 3 h / 1 day before each session) by email
and/or WhatsApp.
- **Attendance register** — attendees × sessions with ticks, totals per session,
completion, filters (completed, missed the last two), Excel/PDF export.
Supervisors can tick someone present or absent (audit-logged), and issue,
send, download or revoke certificates.
- **Scan stats** — recent scans and per-event scan counts.
- **Offline scanning** — while online, a staff member locks an event section (or the
whole event) to their device and downloads its tickets. They can then keep scanning
with no connection; scans are queued on the device and sync automatically when it's
back online (never redeeming more than is left). Only one device can hold a section,
and while it's locked nobody else can scan it, online or offline. Problems found
while syncing go into a report for a supervisor. Supervisors can force-release a
lost device. The scanner can be added to the home screen (PWA) and opens offline.
- **Printing** — A4 sheets (8 tickets per page) and A6 single tickets.
- **Resend** — send tickets to a different email/phone at the door.
---
## Attendee forms
- **Form builder** per event — field types: Yes/No, Text, Paragraph, Number,
Date, plus Statement and Heading blocks; required/optional, help text,
reordering. The whole form can be marked required.
- **Drafts** — partially filled forms are auto-saved.
- **Responses** — view, filter (event, user, past/inactive) and print one
response per A4 page. Supervisors can edit responses.
---
## Messaging & notifications
Every user picks a **notification preference**: Email, WhatsApp, or Both.
**Automatic notifications**
| Trigger | Goes to |
|---------|---------|
| Registration created / updated | Registrant (with invoice if unpaid) + event notify recipients |
| Payment recorded / donation applied / unassigned | Registrant (with receipt) + admins |
| Refund processed | Registrant + admins |
| Tickets generated | Registrant (ticket PDF) |
| Ticket checked in | Ticket holder |
| Daily summary, 07:00, for each live event that hasn't started yet | Registrations inbox + event notify recipients |
| Account registered, activation, login, password changed/reset, account closed | The account holder |
**Bulk messaging** (supervisor)
- **Email attendees** and **WhatsApp attendees** — filter by status or pick
specific attendees, preview, send now or schedule for later.
- **Placeholders** — `{{name}}`, `{{event.title}}`, `{{event.link}}`,
`{{event.start}}`, `{{balance}}`, and `{{payment.link}}` (a live Yoco link for each recipient's
outstanding balance).
- **Broadcasts** — ad-hoc email/WhatsApp to any users or addresses, not tied to
one event.
- **Message type** — Event update or News & announcements; news only reaches people
who opted in (see [Privacy](#privacy-popia)).
- **Automations** — schedule a sequence of lifecycle emails for an event
(pre-event, final reminder, thank-you, promo for another event).
- **Manage scheduled** — view, edit or delete queued jobs per channel. Sends
run in the background and are purged 24 h after sending.
**WhatsApp** is connected through the WAWP API: an admin links a number in
Site Settings → WhatsApp (QR code or pairing code), with status, restart and
logout controls. Disconnected sessions are recovered automatically.
---
## User dashboard (My Events)
- Registrations with status badges, event thumbnails, and Closed/Past/Inactive
tags. Past and closed events are hidden by default.
- Registration detail: itemised bill (early-bird lines grouped), payments,
**Invoice** and **Receipt** buttons, edit, cancel, **Make payment**.
- Upcoming events and tickets side by side, with bulk ticket email/WhatsApp.
- **Ticket transfer** — offer a ticket to someone else before the event by typing
their name and email/phone (no user search, and nothing reveals whether they have an
account). They get a link to accept or decline; the ticket only moves once they
accept, and the sender can cancel until then (offers expire after 7 days or at the
event start). On accepting they get a new ticket and QR code, and an inactive account
with an activation link if they're new. The old code stops working, and scanning it says it
was transferred, both online and in offline-mode sync reports. One place can be split
off a ticket covering several people. Billing stays with the payer. It's not allowed
once a ticket has been scanned, and it can be turned off per event (supervisors can
always transfer, from At the door → Tickets). On youth events the new holder needs
their own parental consent, and the previous holder's form answers are removed.
Every transfer is in the audit log.
- **Payment history** (`/dashboard/user/payments`) — paginated, with filters by
date, method and payment/refund.
- **Donate** page.
- **Forms** — complete outstanding attendee forms.
- **Profile & Security** — edit details and notification preference, change
password, see recent account activity (logins, password changes, device),
sign out everywhere, deactivate or delete my data.
---
## Staff tools
- **Staff dashboard** — scan stats and quick actions.
- **Ticket scanning** (`/dashboard/staff/ticket-scanning`) — camera QR scanner
with event and section filters.
- **Event tickets & printing** (`/dashboard/staff/event-tickets`) — list, search
and print an event's tickets.
---
## Supervisor tools
- **Dashboard** — KPIs (active events, revenue, donations, registrations,
tickets sold, each vs. the previous month), revenue trend chart, top events,
and recent scans. Tiles link to the matching report.
- **Manage events** — the 9-step event wizard, ticket options, variants,
sections, form and attachments.
- **At the door** — search by name/phone/ticket ID, walk-in registration,
payments, a **Check-In** tab for redeeming main tickets by quantity, and
ticket printing/resending.
- **Self-service kiosk** (`/self-service`) — a touchscreen page a supervisor
unlocks for an event, where visitors register themselves. Existing accounts
are matched by email/phone, and any change to their details needs
confirmation.
- **Manual registration** — register someone on their behalf and record their
payment.
- **Payments** — tabs for Payment, Refund, Donations (assign/unassign),
Reconcile (Yoco transactions), and Payment Link.
- **Coupon codes** (`/dashboard/supervisor/coupons`) — create, edit and turn off
discount codes, and see who used each one.
- **Rooms & groups** (`/dashboard/supervisor/allocations`):
- Per event, allocation sets such as "Rooms", "Small groups" or "Buses". Each set
has groups with an optional capacity, gender restriction and leader (staff).
- A drag-and-drop board, plus auto-allocate: fill evenly, respecting capacity and
gender, keeping bookings together or splitting them.
- Printable per-group and master lists. On youth events they can include medical
details, and viewing those is audited.
- The group can show on tickets and in My Events.
- Email and WhatsApp attendees can be filtered to one group.
- Leaders see their people under Staff → **My groups**.
- Works on named attendees. Events can ask each person's gender.
- **Offline scanning** (`/dashboard/supervisor/offline-scans`) — devices holding
offline locks (with force release) and sync reports to review.
- **Waitlist** (`/dashboard/supervisor/waitlist`) — the queue per event: offer a
place now, move people up or down, or remove them.
- **Parental consent** (`/dashboard/supervisor/consent`) — consent status per
attendee on youth events, resend (one or all pending), re-request after the
consent text changes, and a printable medical & emergency list (audited).
- **Email attendees / WhatsApp attendees** — bulk and scheduled messaging.
- **Forms** — view, filter, edit and print form responses.
- **Reports** — see [Reports](#reports).
- **Site banner** (`/set-banner`) — set the announcement bar.
---
## Admin tools
- **Admin dashboard** — same KPIs as supervisor, plus admin quick actions.
- **Manage users** — server-side search and filters (role, active), paging,
edit role and notification preference, revoke sessions, delete or
**anonymise** personal data.
- **Data requests** — POPIA correction and deletion requests (see
[Privacy](#privacy-popia)).
- **Registrations** — all registrations, with filters (event, status, text),
status counts, revenue and outstanding totals, and per-registration options,
form answers and payments (with who recorded each one).
- **Cashup** — see below.
- **Audit log** (`/dashboard/admin/audit-log`) — who did what and when for
refunds, donation assign/unassign, manual registrations, staff
cancellations, event create/update/delete, settings changes, and coupon codes
(created, edited, turned off, applied or removed by staff).
- **Site settings** — see below.
---
## Cashup & event costs
- **Event costs** — once-off or per-item costs (optionally linked to a ticket
option), with a "paid from takings" method so cash on hand is correct.
- **Expected vs. actual** — expected takings per method (cash/card/EFT/other)
compared with actual amounts, with variance shown.
- **Cash denomination counts** — entered per staff member, at any time. The
event's actual cash is the sum of these counts.
- **Payment accountability by staff member** — what each person recorded, by
method, with live cash variance.
- **Close / quick close / reopen** an event — closing locks it from further
edits and hides it from the kiosk and user actions. Drafts are saved as you
go.
- **Report tab** — a read-only cashup summary, shown automatically once the
event is closed.
- **Cashup audit trail** — every close/reopen with totals and who did it.
---
## Reports
A full-width workspace with shared filters (events, date range with presets,
include past/inactive/closed) and a searchable grid of reports, each opening in
a viewer with its own filters, stat tiles and bar charts.
| Category | Reports |
|----------|---------|
| Financial | Payments between dates · Revenue summary (by method) · Revenue detailed · Finance report (revenue & costs, after discounts) · Profit report · Discounts (coupon codes) |
| Registration | Registration funnel (views → started → submitted → paid, daily trend, by source / device / ticket option) · Attendees per event (grouped by option) · Registration type counts · Master orders breakdown · Registration status breakdown (count by registration or ticket quantity) · Parental consent status · Medical & emergency list · Feedback summary (response rate, average ratings, choice breakdowns, comments; compare events) |
| Ticketing | Ticket usage summary |
| Donations | Donations breakdown (used / unused per event, plus the sponsorship pool) |
| Cashup | Cashup reconciliation · Cashup audit trail |
**Export:** Print, branded PDF download, styled Excel (`.xlsx`) workbook,
Email (to yourself), or WhatsApp (the PDF to your own number). Dashboard tiles
deep-link into reports with `?report=&range=`.
---
## Site settings & setup wizard
**Setup wizard** (`/setup`) — on a fresh database, walks through organisation
details, the first admin account and branding. No manual seeding needed.
**Site Settings** (`/dashboard/admin/settings`) — stored in the database and
applied without a restart. Secrets are encrypted with AES-256-GCM.
| Tab | Contains |
|-----|----------|
| Organisation | Name, tagline, contact email/phone, address (with map link) |
| Branding | Primary/secondary/accent colours, logo, favicon, colours suggested from the logo, live website/email preview on desktop and mobile |
| Notifications | Registrations inbox and admin notification emails |
| Email | SMTP host/port/TLS/user/password/from, with a "Test connection" button |
| Legal | Operator, Information Officer, website, effective date for the Terms/Privacy pages |
| WhatsApp | WAWP credentials, connect a number, status/restart/logout |
| Payments | Online payment providers: Enabled toggle and order; PayFast merchant ID/key, passphrase and sandbox/live; Yoco's secret key (masked, test/live), webhook secret (generate by registering with Yoco, or enter), list/delete the account's webhooks |
| Backups | List, run now, download database backups |
See [SETTINGS.md](./SETTINGS.md) for the default of every setting.
---
## Accounts & security
- Register, log in, forgot/reset password, activate account (for accounts
created on someone's behalf).
- JWT authentication (HS256 pinned) with **session revocation** (token
versioning) — users can sign out everywhere, and admins can do it for any user.
- **Two-factor authentication** — a 6-digit login code by email or WhatsApp
(10-minute expiry, 5 attempts, resend), with "Trust this device for 30 days".
**Required for Admins and Supervisors**: their next login sends a code that turns
it on, and they can change the channel but not turn it off (skipped only when no
email or WhatsApp channel can deliver a code; `TWO_FACTOR_REQUIRED_ROLES=false`
is the emergency switch). Optional for everyone else from Profile & Security.
Admins can reset it for a locked-out user, and the kiosk asks for the code at
setup.
- **Login lockout** after repeated failed attempts, plus login rate limiting
(`TRUST_PROXY` to read real IPs behind a proxy).
- **Security event log** — logins, password changes and resets, with device
and IP.
- **Account closure** — deactivate, or close and ask for the personal data to be
deleted (see [Privacy](#privacy-popia)).
- **Upload hardening** — server-generated filenames, and uploads served with a
sandbox CSP and `nosniff`.
---
## Privacy (POPIA)
Tools that help a church handle personal information the way POPIA expects. They
support compliance; wording and retention choices are for the church's Information
Officer.
**For everyone** — the **Privacy** page (`/dashboard/user/privacy`):
- **News & announcements** opt-in per channel (email, WhatsApp), with the history of
every choice: when, and how (sign-up, privacy page, unsubscribe link, STOP reply…).
Sign-up has an unticked opt-in box; accounts that never answered are asked once
after logging in.
- **Download my data** — readable PDF, or the complete copy as JSON.
- **Correct my data** — profile fields directly; anything else as a correction request.
- **Delete my account** — closes the account at once and files a deletion request.
**Bulk messages** are either an **Event update** (about an event the recipients
registered for; always sent) or **News & announcements** (only to people who opted in
on that channel and haven't unsubscribed). News emails carry an unsubscribe link and
one-click `List-Unsubscribe` headers; news WhatsApps end with "Reply STOP" and a link.
Event updates say why they were sent and link to the preferences. A STOP reply on
WhatsApp turns off WhatsApp news (through WAWP's `message` webhook, checked against a
secret in the webhook URL).
**Privacy notice** (`/legal/privacy`) — edited in **Admin → Privacy (POPIA)**
(`/dashboard/admin/privacy`): sections start from the built-in text; drafts, preview,
and published, numbered versions (earlier ones stay readable with `?version=N`).
Placeholders (`{org}`, `{ioName}`, `{ioEmail}`, `{website}`, `{orgEmail}`,
`{medicalRetentionDays}`, `{operators}`) are filled in from Site settings and the
operator register. A version marked as a significant change is accepted once by everyone
after their next login; sign-up records the version accepted. The contact page shows the
Information Officer.
**Registers and logs** (same admin page):
- **Operators** — the services that process personal information (payment, WhatsApp,
email, hosting, maps, error monitoring), what each receives and where; those in use
are listed in the notice.
- **Consent log** — news opt-ins/opt-outs, privacy notice acceptances and parental consent
answers, searchable.
- **Access log** — every bulk export or view of personal information (reports, attendance
registers, certificates, group lists, form answers, consent and medical lists, backups)
and every "view as" session, with the number of records. Children's and medical
information is flagged (medical lists, and anything about a youth event).
- **Retention** — how long each kind of personal information is kept, applied by a nightly
job: inactive accounts anonymised after 5 years (warning 30 days before; never staff
roles or people with an upcoming registration), form answers deleted 12 months after
the event, login history kept 24 months, payer details in payment notifications removed
after 24 months, audit/access logs kept 5 years. The notice's `{retention}` placeholder
lists them.
- **Breaches** — the register of security compromises, with when the Information Regulator
and the people affected were told, and drafts of both notifications.
- **Readiness** — a scored checklist of what's set up, each item linking to its fix.
**For admins** — **Data requests** (`/dashboard/admin/data-requests`): correction and
deletion requests with a 30-day due date, emailed to the Information Officer and
admins, reminded daily when due within a week. Completing a deletion anonymises the
person: name, contact details, the people they registered, form answers, consent
details, sponsor names, login history and payer details in provider notifications are
removed; payments, registrations, tickets and check-ins stay as "Deleted User". It
waits while they have a registration for an upcoming event. Everything is in the audit
log.
---
## Help system
A floating **"Need help?"** button on every page opens a page-specific guide
(tabs, quick links). Every page has dedicated content (`frontend/src/content/help/`),
falling back to a general guide.
---
## Background jobs
Timed jobs run on a job queue in Postgres (pg-boss, `backend/src/jobs/index.js`): each
runs once per schedule even with several server processes, every run is recorded,
failed runs are retried twice, and a slow run never overlaps the next. Admin → Site
Settings → Background jobs shows each job's schedule, next run and last 10 runs, with
"Run now". Times are in `APP_TIMEZONE` (default Africa/Johannesburg). Change a schedule
with `JOB_SCHEDULE_<NAME>` (cron); `JOBS_ENABLED=false` turns them all off. If the queue
can't start, the same jobs run on in-process timers.
| Job | Schedule |
|-----|----------|
| Scheduled email/WhatsApp worker | Polls every 30 s (`SCHEDULED_EMAILS_INTERVAL_MS`) and purges sent jobs after 24 h (not on the queue) |
| `daily-summary` — daily event summary email | 07:00 daily (`DAILY_SUMMARY_ENABLED=false` to disable) |
| `waitlist` — expire unclaimed offers, offer freed places | Every 10 minutes (`WAITLIST_JOB_ENABLED=false`) |
| `payment-status` — look up PayFast checkouts (10 min – 3 days old) whose notification never arrived, and record them | Every 15 minutes (`PAYMENT_STATUS_JOB_ENABLED=false`) |
| `feedback` — post-event feedback forms and their 3-day reminder | Every 15 minutes (`FEEDBACK_JOB_ENABLED=false`) |
| `sessions` — session reminders, and certificates for events that ended in the last 14 days | Every 5 minutes (`SESSION_JOBS_ENABLED=false`) |
| `consent` — parental consent reminders, and deleting medical details after events | 09:00 daily (`CONSENT_JOBS_ENABLED=false`) |
| `privacy` — remind admins of privacy requests due within a week or overdue | 08:00 daily (`PRIVACY_JOBS_ENABLED=false`) |
| `retention` — delete/anonymise personal information past its retention period (Admin → Privacy → Retention) | 03:30 daily (`RETENTION_JOBS_ENABLED=false`) |
| `analytics-rollup` — roll raw visit rows older than 90 days into daily totals | 04:15 daily (`ANALYTICS_ROLLUP_ENABLED=false`) |
| `backup` — database backup (`pg_dump`, gzipped, 14-day retention) | 02:00 daily (`BACKUP_ENABLED=false`) |
| `temp-cleanup` — temp files older than 7 days in `/temp` | 03:00 daily |
---
## Operations
- **PM2** (`ecosystem.config.js`) runs backend and frontend, restarting either
one above 500 MB of memory.
- **Sentry** error monitoring when `SENTRY_DSN` / `NEXT_PUBLIC_SENTRY_DSN` are
set.
- Unhandled promise rejections are logged and don't crash the server.
- Backend status page (`/`) and built-in API docs page (`/docs`).
- **Tests** — Jest unit tests in `backend/tests` (`npm test`), and Playwright
end-to-end tests in `e2e/` (`npm run test:e2e`) that start their own backend and
frontend against a separate, freshly seeded database: public pages, free and paid
registration through to check-in and cashup, multi-day check-in, the attendance
register and certificates, the event wizard, the kiosk, and the admin jobs page.
- **CI** — `.gitea/workflows/ci.yml` runs the backend tests, frontend lint/typecheck/
build and the end-to-end tests on every push (needs a Gitea Actions runner). A `v*`
tag also deploys over SSH with `scripts/deploy.sh` once the `DEPLOY_*` secrets are set:
it backs up the database, checks out the tag, migrates, builds and reloads PM2, and
goes back to the previous version if a step fails before the reload.
- Versioning via git tags and [CHANGELOG.md](./CHANGELOG.md).
---
## Page map
| Path | Who | What |
|------|-----|------|
| `/` | Public | Home |
| `/events`, `/events/[id]` | Public | Event list and detail |
| `/[alias]` | Public | Event short link |
| `/register/[eventId]` | Public / User | Registration |
| `/registration/success` | Public / User | Confirmation, form and pay |
| `/pay/[id]` | Public | Pay online: choose a provider, or go on to PayFast |
| `/payment/success`, `/payment/failure`, `/payment/cancel` | Public | Return pages after paying online |
| `/forms` | Public / User | Attendee forms |
| `/contact`, `/legal/terms`, `/legal/privacy`, `/lockdown-rules` | Public | Info pages |
| `/login`, `/register`, `/forgot-password`, `/reset-password`, `/activate-account` | Public | Accounts |
| `/setup` | First run | Setup wizard |
| `/consent/[token]` | Parent/guardian | Give or decline parental consent |
| `/waitlist/claim/[token]` | Public | Claim a waitlist place |
| `/certificates/verify/[code]` | Public | Check a completion certificate |
| `/unsubscribe?t=…` | Public | Unsubscribe from news (link in news emails/WhatsApps) |
| `/self-service` | Supervisor-unlocked | Kiosk |
| `/set-banner` | Supervisor | Site banner |
| `/dashboard/user` (+ `/payments`, `/donate`, `/forms`, `/profile`, `/privacy`, `/reset-password`) | User | My Events |
| `/dashboard/staff` (+ `/ticket-scanning`, `/event-tickets`) | Staff | Staff tools |
| `/dashboard/supervisor` (+ `/events`, `/event-options`, `/at-the-door`, `/manual`, `/manual-registration`, `/payments`, `/coupons`, `/waitlist`, `/attendance`, `/consent`, `/offline-scans`, `/allocations`, `/email-attendees`, `/whatsapp-attendees`, `/forms`, `/reports`) | Supervisor | Supervisor tools |
| `/dashboard/admin` (+ `/users`, `/registrations`, `/forms`, `/cashup`, `/cashup/[id]`, `/audit-log`, `/data-requests`, `/privacy`, `/settings`) | Admin | Admin tools |