- 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
788 lines
41 KiB
Markdown
788 lines
41 KiB
Markdown
# Hope Events — Road to 2.0.0
|
||
|
||
Planned features and platform work to take Hope Events from 1.32.x to 2.0.0.
|
||
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, and [FEATURES-ROUND-1.md](./FEATURES-ROUND-1.md) for the
|
||
previous round (1.12.0 – 1.23.0). Where to pick up: [HANDOVER.md](./HANDOVER.md).
|
||
|
||
**Theme for 2.0:** turn a feature-rich church app into a product that can be
|
||
sold — trustworthy (POPIA), flexible (multi-day events, several payment
|
||
providers), and maintainable (tests, CI, typed backend, versioned API).
|
||
|
||
**Status (2026-10-23):** all features in this document are approved. Shipped
|
||
so far: 1.32.0 early-bird ribbon on event cards; 1.33.0 multi-day events
|
||
phase A (sessions, late joining, per-session check-in, attendance register,
|
||
reminders, certificates); 1.34.0 job queue (pg-boss), Playwright E2E tests, Gitea CI
|
||
workflow (waiting for a runner) and deploy script; 1.35.0 multi-day events phase B
|
||
(pick sessions, per-session capacity), with 1.35.1 adding the per-session waitlist, the session
|
||
picker at the door and session choice on "All sessions" tickets; 1.36.0 payment provider interface
|
||
(Yoco moved behind it, Site settings → Payments); 1.37.0 PayFast and the "Pay online" page;
|
||
1.37.1 "Pay online" hidden when it can't be used; 1.37.2 payment status checks for missing
|
||
notifications. SnapScan and Zapper are dropped — Yoco and PayFast are enough (2026-10-23);
|
||
1.38.0 POPIA toolkit part 1 (privacy page, unsubscribe, WhatsApp STOP, data requests); 1.39.0 part 2 (privacy notice editor and versions, operator register, consent and access logs); 1.40.0 part 3 (retention policies and job, breach register, children’s-data flags, readiness page).
|
||
**Next: 1.41.0 install/update tooling + the Hope Events Hub (§13).**
|
||
|
||
**Repositories:**
|
||
|
||
| Repo | What |
|
||
|------|------|
|
||
| `joshua/hope-events` (this repo) | The church app. Everything in this document except the Hub app itself — including the Hub *client* (licence step, "Connect to Hub", heartbeat, licence checks) |
|
||
| `joshua/hope-events-hub` (new) | The Hub app (§13): its own versions, CHANGELOG, releases and server |
|
||
|
||
---
|
||
|
||
## Contents
|
||
|
||
1. [Multi-day events (sessions)](#1-multi-day-events-sessions) ← **first**
|
||
2. [Payment providers: PayFast](#2-payment-providers-payfast)
|
||
3. [POPIA toolkit](#3-popia-toolkit)
|
||
4. [Kids check-in with security labels](#4-kids-check-in-with-security-labels)
|
||
5. [Volunteer / serving rosters](#5-volunteer--serving-rosters)
|
||
6. [Event series and cloning](#6-event-series-and-cloning)
|
||
7. [Apple and Google Wallet tickets](#7-apple-and-google-wallet-tickets)
|
||
8. [Outbound webhooks](#8-outbound-webhooks)
|
||
9. [Admin dashboard, command palette, bulk actions](#9-admin-dashboard-command-palette-bulk-actions)
|
||
10. [Languages (Afrikaans first)](#10-languages-afrikaans-first)
|
||
11. [Accessibility pass (WCAG 2.2 AA)](#11-accessibility-pass-wcag-22-aa)
|
||
12. [Multi-campus (one install per church)](#12-multi-campus-one-install-per-church)
|
||
13. [Central management platform (Hope Events Hub)](#13-central-management-platform-hope-events-hub)
|
||
14. [Foundations (platform work)](#14-foundations-platform-work)
|
||
15. [Suggested build order](#suggested-build-order)
|
||
16. [Decisions](#decisions-2026-10-22)
|
||
|
||
---
|
||
|
||
## 1. Multi-day events (sessions)
|
||
|
||
One event that runs over several dates, e.g. **"Alpha Course — every Wednesday
|
||
18:30–20:30 for 5 weeks"**, a 3-day conference, or a camp with an evening
|
||
session each day. People register and pay **once**, get **one ticket**, and are
|
||
checked in **per session**.
|
||
|
||
> Not the same as [Event series](#6-event-series-and-cloning). A series groups
|
||
> *separate* events (Camp 2026, Camp 2027) that each have their own
|
||
> registrations. Sessions are dates *inside one* event.
|
||
|
||
**Who:** Supervisors set up the schedule. Users see it on the event page, in
|
||
their tickets and in their calendar. Staff check in per session.
|
||
|
||
### Event setup
|
||
|
||
New **Schedule** section on the event form:
|
||
|
||
| Setting | Options |
|
||
|---------|---------|
|
||
| Schedule type | Single date (today's behaviour) · Multiple sessions |
|
||
| Pattern builder | Repeat **daily / weekly / every N weeks** on chosen weekday(s), start date, time from–to, **number of sessions** or **until date** |
|
||
| Generated sessions | Editable list: change date/time/location/label of any one session, delete, or add an extra one-off date |
|
||
| Session label | Optional, e.g. "Week 1 — Is there more to life?" (defaults to "Session 1, 2, …") |
|
||
| Session location | Optional; falls back to the event location |
|
||
| Attendance mode | **Whole course** (one registration covers all sessions) · **Pick sessions** (see phase B below) |
|
||
| Late joining | Per event: Off (registration closes when the first session starts) · Allow until the start of session N |
|
||
| Late-joiner price | Per event, only when late joining is on: Full price · **Pro-rata** (price × remaining sessions ÷ total sessions, rounded to the **nearest R10**, e.g. R183 → R180, R185 → R190) · Fixed late price |
|
||
| Check-in window | Per event: **Whole day** (default — any time on the session's date, in case someone was let in without being scanned) · Session time only (2 h before start until the session ends) |
|
||
| Session reminders | Per event, **off by default**: when on, choose timing (e.g. 3 h before, the day before at 18:00, or both) and channels (email / WhatsApp) |
|
||
| Certificates | Per event, **off by default**: when on, choose a template and the completion rule (see [Certificates](#certificates)) |
|
||
|
||
The pattern builder is only a helper: it generates rows, then the session list
|
||
is the source of truth. Typing "every Wednesday for 5 weeks" and then skipping
|
||
a public holiday is just deleting one row.
|
||
|
||
### How it works
|
||
|
||
- `Event.startDate` / `endDate` **stay** and are kept in sync automatically
|
||
(first session start, last session end). Everything that already reads them —
|
||
consent age checks, transfer cut-off, early-bird, feedback timing, ICS,
|
||
listings, sitemap — keeps working unchanged. Only places that need
|
||
per-session behaviour are updated.
|
||
- **Public event page:** shows the full schedule, with past sessions greyed out
|
||
and a "Next session" highlight. Event cards show "5 sessions · Wed 4 Nov –
|
||
Wed 2 Dec".
|
||
- **Event listings:** a multi-session event stays "upcoming / running" until
|
||
its **last** session ends, and sorts by its **next** session.
|
||
- **Tickets (PDF, email, WhatsApp):** list all session dates. One QR code.
|
||
- **Calendar (ICS):** one VEVENT per session (not an RRULE, so edited or
|
||
skipped sessions are exact).
|
||
- **Check-in:** the scanner auto-selects today's session (on a day with more
|
||
than one session, the one happening now or next), with a dropdown to
|
||
override. Whether a scan is accepted depends on the event's **check-in
|
||
window** (whole day or session time only). A ticket can be checked in
|
||
**once per session** (per person for group tickets). Scanning again in the
|
||
same session shows "Already checked in for Session 3 at 18:42".
|
||
- **Manual check-in:** supervisors can tick someone as present for a session
|
||
from the attendance register afterwards (e.g. let in without a scan), logged
|
||
in the audit log with who marked it.
|
||
- **Reminders:** off by default; when switched on for the event, a reminder
|
||
goes out before **each** session (email/WhatsApp, via the scheduled-message
|
||
worker), e.g. "See you tonight at 18:30".
|
||
- **Late joiners:** when late joining is on, the registration page shows which
|
||
sessions are left and the late price ("3 of 5 sessions left — R180").
|
||
- **Changes:** moving or cancelling a session after people registered prompts
|
||
the supervisor to notify registrants (pre-filled broadcast), and updates the
|
||
ICS/tickets.
|
||
|
||
### Phase B: pick sessions (drop-in)
|
||
|
||
Second step, released separately. Lets people register for **some** sessions,
|
||
e.g. a conference where each day is sold separately or a weekly drop-in class.
|
||
|
||
- Ticket options can be scoped to sessions: "Day pass (pick days)" priced per
|
||
session, "Full conference pass" covering all.
|
||
- Optional **capacity per session** on top of the option stock limit.
|
||
- The ticket stores which sessions it covers; scanning on a session it doesn't
|
||
cover shows "Not registered for this session" (with "Add & charge" at the
|
||
door for supervisors).
|
||
- Waitlist works per session when sessions have their own capacity.
|
||
|
||
### Reporting
|
||
|
||
- **Attendance register:** grid of attendees × sessions with ticks, totals per
|
||
session, attendance % per person. Export to Excel and PDF.
|
||
- **Completion filter:** "attended at least 4 of 5" — useful for certificates
|
||
and for follow-up ("missed the last 2 weeks").
|
||
- Daily summary email includes "Sessions today".
|
||
- Funnel stats unchanged (they are per event).
|
||
|
||
### Certificates
|
||
|
||
Optional completion certificates (PDF), chosen per event.
|
||
|
||
- **3–4 built-in templates**, e.g. *Classic* (border, serif), *Modern* (clean,
|
||
accent colour bar), *Church* (logo-led, scripture line), *Kids* (playful,
|
||
for children's programmes). All use the site logo and brand colour from Site
|
||
settings.
|
||
- **Event settings:** Certificates on/off · Template · Title ("Certificate of
|
||
Completion") · Body text with placeholders (`{name}`, `{event}`,
|
||
`{sessionsAttended}`, `{totalSessions}`, `{endDate}`) · Signature name and
|
||
role (optional uploaded signature image) · **Completion rule**: attended at
|
||
least N sessions or N % (default: all sessions).
|
||
- **Preview** button in the event form renders a sample PDF.
|
||
- **Who gets one:** attendees meeting the completion rule after the last
|
||
session. Supervisors can issue/withdraw one manually (audit logged).
|
||
- **Delivery:** download from My Events, bulk download (ZIP) from the
|
||
attendance register, and optional email/WhatsApp send.
|
||
- Each certificate has a short verification code + QR linking to a public
|
||
"certificate valid" page (name, event, date only).
|
||
- Generated with `pdfkit`, like tickets and receipts (`utils/pdfDocs.js`).
|
||
- Also usable on single-date events (completion = checked in).
|
||
|
||
### Edge cases
|
||
|
||
| Case | Behaviour |
|
||
|------|-----------|
|
||
| Delete a session that has check-ins | Blocked; offer **Cancel session** instead (kept for history, hidden from schedule, excluded from attendance %) |
|
||
| Switch Multiple → Single after registrations | Allowed only if no session check-ins exist |
|
||
| Ticket transfer | Allowed until the **first** session starts (current rule). Optional later: allow mid-course transfers with check-in history kept on the original holder |
|
||
| Parental consent | Age is measured at the first session (current `startDate` rule) |
|
||
| Medical details deletion | Runs after the **last** session (end date), as today |
|
||
| Feedback form | Sent after the last session |
|
||
| Cashup | Still per event. At-the-door cash per session is captured with the session on the payment so reports can split it |
|
||
| Offline scanning | Session list and per-session usages are part of the offline lock bundle; sync keeps the session id per scan |
|
||
| Timezones | Sessions stored as UTC instants, same as `Event.startDate` today (see `icsUtils.js`) |
|
||
| Session overlaps / out of order | Allowed (conferences have parallel tracks later), list always sorted by start |
|
||
| Kiosk / At the door | Shows the current session; check-in counts against it |
|
||
|
||
### Data model (sketch)
|
||
|
||
```prisma
|
||
enum ScheduleType { single sessions }
|
||
enum AttendanceMode { whole pick }
|
||
|
||
enum LatePricing { full prorata fixed }
|
||
enum CheckInWindow { whole_day session_only }
|
||
|
||
model Event {
|
||
// ...existing fields
|
||
scheduleType ScheduleType @default(single)
|
||
attendanceMode AttendanceMode @default(whole)
|
||
lateJoinEnabled Boolean @default(false)
|
||
lateJoinUntilSessionId String?
|
||
latePricing LatePricing @default(full)
|
||
lateFixedPrice Float?
|
||
checkInWindow CheckInWindow @default(whole_day)
|
||
sessionReminders Json? // null = off; { offsets: [...], channels: [...] }
|
||
certificatesEnabled Boolean @default(false)
|
||
certificateConfig Json? // template, title, body, signature, completion rule
|
||
sessions EventSession[]
|
||
certificates Certificate[]
|
||
}
|
||
|
||
model Certificate {
|
||
id String @id @default(uuid())
|
||
eventId String
|
||
attendeeId String?
|
||
userId String?
|
||
name String
|
||
code String @unique // verification code
|
||
manual Boolean @default(false)
|
||
issuedAt DateTime @default(now())
|
||
revokedAt DateTime?
|
||
event Event @relation(fields: [eventId], references: [id], onDelete: Restrict)
|
||
|
||
@@index([eventId])
|
||
}
|
||
|
||
model EventSession {
|
||
id String @id @default(uuid())
|
||
eventId String
|
||
label String?
|
||
startsAt DateTime
|
||
endsAt DateTime
|
||
location String?
|
||
capacity Int? // phase B
|
||
cancelledAt DateTime?
|
||
order Int @default(0)
|
||
event Event @relation(fields: [eventId], references: [id], onDelete: Cascade)
|
||
usages TicketUsage[]
|
||
ticketSessions TicketSession[]
|
||
|
||
@@index([eventId, startsAt])
|
||
}
|
||
|
||
model TicketUsage {
|
||
// ...existing fields
|
||
sessionId String? // null on single-date events
|
||
session EventSession? @relation(fields: [sessionId], references: [id], onDelete: Restrict)
|
||
@@index([sessionId])
|
||
}
|
||
|
||
// Phase B only: which sessions a "pick sessions" ticket covers.
|
||
model TicketSession {
|
||
ticketId String
|
||
sessionId String
|
||
@@id([ticketId, sessionId])
|
||
}
|
||
```
|
||
|
||
`Ticket.isUsed` keeps its meaning for single-date events. For session events
|
||
it's not used for check-in decisions; "checked in" is per session
|
||
(`TicketUsage.sessionId`).
|
||
|
||
### Plumbing
|
||
|
||
- Session CRUD + pattern generator in `backend/src/utils/sessions.js` with
|
||
Jest tests (DST-free but still test month boundaries, skipped dates, "until"
|
||
vs "count").
|
||
- Scan endpoint takes an optional `sessionId`; defaults to the current session.
|
||
- Help content, audit log entries (session created/moved/cancelled),
|
||
CHANGELOG, FEATURES.md, SETTINGS.md.
|
||
|
||
---
|
||
|
||
## 2. Payment providers: PayFast
|
||
|
||
Add **PayFast** next to Yoco, and let the admin **enable one or both** in Site
|
||
settings. At checkout the user picks from the enabled ones. (Shipped in
|
||
1.36.0–1.37.2. SnapScan and Zapper were planned too, but dropped on 2026-10-23:
|
||
two providers are enough.)
|
||
|
||
**Who:** Admins configure providers. Users choose at payment. Supervisors
|
||
reconcile.
|
||
|
||
### Site settings → Payments
|
||
|
||
| Setting | Options |
|
||
|---------|---------|
|
||
| Providers | Yoco · PayFast — each with an **Enabled** toggle |
|
||
| Credentials | Per provider (merchant id/key, API key, passphrase, webhook secret…), stored encrypted (`utils/encryption.js`, same as WhatsApp) |
|
||
| Mode | Live / Sandbox per provider (where supported) |
|
||
| Display order / default | Which one is shown first. Default order: Yoco · PayFast |
|
||
| Minimum amount | **R15 for all providers** (one site-wide minimum for online payments) |
|
||
|
||
**Fees are not handled automatically.** Payments are recorded at the full
|
||
amount paid; provider fees are not deducted, estimated or added to the payer's
|
||
total. Fees are dealt with by people during **cashup**, as today. There is no
|
||
"pass fee to payer" option.
|
||
|
||
"Test connection" button per provider, like the WhatsApp setup.
|
||
|
||
### How it works
|
||
|
||
- A single **PaymentProvider interface** in the backend:
|
||
`createCheckout`, `handleWebhook` (verify signature → mark paid),
|
||
`getTransaction`, `refund` (if the provider supports it via API),
|
||
`listTransactions` (for reconciliation).
|
||
- Yoco is moved behind the same interface first (no behaviour change), then
|
||
PayFast is added.
|
||
- `Payment.method` gets the provider name; `YocoTransaction` is generalised to
|
||
`ProviderTransaction` (`provider` column) so the existing reconciliation page
|
||
works for all providers.
|
||
- Refunds: if the provider has no refund API, the supervisor refunds in the
|
||
provider's portal and records it here (existing manual refund flow).
|
||
|
||
### Edge cases
|
||
|
||
- Provider disabled while a checkout is in progress → webhook still accepted
|
||
and processed.
|
||
- Duplicate / late webhooks → idempotent on the provider's transaction id.
|
||
- Webhook never arrives → "Check payment status" button and a periodic
|
||
reconcile job.
|
||
- Amount mismatch (user edits amount in app) → payment held as "needs review".
|
||
- Only enabled providers appear in kiosk / self-service.
|
||
- Amount below R15 → online payment options hidden, with a message to pay at
|
||
the door / contact the organiser.
|
||
- **Before building each one:** get the documentation links from Joshua and
|
||
confirm current API docs for webhooks, sandbox, refunds and QR deep-link
|
||
support.
|
||
|
||
---
|
||
|
||
## 3. POPIA toolkit
|
||
|
||
Tools that make the platform **POPIA-ready** out of the box. This is the main
|
||
selling point for other churches and organisations: "your attendee data is
|
||
handled properly, and you can prove it."
|
||
|
||
> This is tooling to support compliance, not legal advice. Wording and
|
||
> retention periods should be checked by whoever is the customer's
|
||
> Information Officer.
|
||
|
||
**Who:** Admins (and the appointed Information Officer) manage it. Users get
|
||
self-service privacy tools.
|
||
|
||
### For users (data subjects)
|
||
|
||
- **Privacy centre** in My account:
|
||
- **Download my data** — JSON + readable PDF of profile, registrations,
|
||
payments, tickets, form answers, consents, messages sent.
|
||
- **Correct my data** — edit profile; request correction of locked records.
|
||
- **Delete my account** — creates a deletion request (see below).
|
||
- **Communication preferences** — email / WhatsApp / marketing opt-in with
|
||
timestamps (extends the existing notification preferences).
|
||
- **Unsubscribe link** in every bulk email and an opt-out keyword for bulk
|
||
WhatsApp. Opted-out users are skipped automatically in broadcasts.
|
||
- **Privacy notice acceptance** — versioned; users accept the current version
|
||
at registration, and are asked again when it changes.
|
||
|
||
### For admins
|
||
|
||
| Tool | What it does |
|
||
|------|--------------|
|
||
| Information Officer | Name, contact details, shown on privacy notice and contact page |
|
||
| Privacy notice editor | Versioned text, published version history |
|
||
| Consent log | Who consented to what, when, which notice version, how (web, kiosk, WhatsApp) |
|
||
| Data requests inbox | Access, correction and deletion requests with status, due dates and audit trail |
|
||
| Deletion workflow | Admin approves → personal info is **anonymised**; financial records kept (amounts, dates) but unlinked from the person, as tax law requires them to be kept |
|
||
| Retention policies | Per data category (inactive accounts, form answers, medical details, analytics, audit log, backups): keep for N months, then anonymise/delete via nightly job. Sensible defaults shipped |
|
||
| Operator register | List of third parties that process data (Yoco/PayFast/…, WhatsApp provider, email provider, Sentry, hosting) with purpose; feeds the privacy notice |
|
||
| Access log | Who exported or viewed personal data in bulk (exports, attendee lists, medical details) |
|
||
| Breach register | Record a security compromise, affected users, actions taken; templates for notifying users and the Information Regulator |
|
||
| Children's data | Already covered by parental consent; flagged as special/children's info in reports |
|
||
| POPIA readiness page | Checklist with score: officer set, notice published, retention set, operators listed, unsubscribe working… |
|
||
|
||
### Edge cases
|
||
|
||
- Deletion of a user with an open registration / upcoming event → request
|
||
waits until after the event, or the admin cancels and refunds first.
|
||
- Anonymised users still count in historic reports ("Deleted user").
|
||
- Backups contain deleted data until they age out — stated in the notice;
|
||
backup retention is one of the retention policies.
|
||
- Impersonation and exports must be logged in the access log.
|
||
|
||
---
|
||
|
||
## 4. Kids check-in with security labels
|
||
|
||
A secure check-in/check-out flow for children's ministry and youth events.
|
||
**Builds on sessions (§1), attendees, parental consent and allocation groups.**
|
||
|
||
**Who:** Parents check in children (self-service kiosk or staff desk).
|
||
Kids-ministry staff run rooms. Supervisors set up rooms.
|
||
|
||
### How it works
|
||
|
||
- Parent finds their family (phone number / QR from My account) and ticks the
|
||
children being dropped off. New children can be added on the spot.
|
||
- The system prints **two labels**:
|
||
- **Child label:** name, room, allergy/medical icons, short pickup code.
|
||
- **Parent label:** the same pickup code.
|
||
- **Check-out** requires scanning or typing the matching code. A mismatch
|
||
alerts the room leader.
|
||
- Rooms come from allocation groups (age/grade rules), with **ratios** (e.g.
|
||
1 leader per 5 toddlers) and live room counts.
|
||
- **Page a parent:** room leader taps "Call parent" → WhatsApp/SMS to the
|
||
parent ("Please come to the Toddlers room").
|
||
- Attendance per child per session feeds the attendance register.
|
||
|
||
### Plumbing / edge cases
|
||
|
||
- Label printing via browser print with label-size CSS (Dymo / Brother label
|
||
printers, 62 mm and 89×36 mm presets); PDF fallback.
|
||
- Allergies and medical info come from the consent/medical fields; respect
|
||
the medical-info deletion rules.
|
||
- Custody notes ("Do not release to …") visible to room leaders only.
|
||
- Works on the existing offline-scanning foundations for flaky Wi-Fi.
|
||
|
||
---
|
||
|
||
## 5. Volunteer / serving rosters
|
||
|
||
Plan who serves where, for an event or each session.
|
||
|
||
- **Serving roles** per event/session: Welcome, Parking, Kids, Sound, Kitchen…
|
||
with the number of people needed.
|
||
- Volunteers **sign up** for open slots, or supervisors **assign** people.
|
||
- Reminders via email/WhatsApp before their slot; "can't make it" → swap
|
||
request to others in the role.
|
||
- **Volunteer check-in** on the day (separate from attendee check-in).
|
||
- Volunteers can get a free/discounted ticket automatically (uses coupons/comp).
|
||
- Reports: roster sheet per session, fill rate, hours served per person.
|
||
|
||
---
|
||
|
||
## 6. Event series and cloning
|
||
|
||
- **Clone event:** copy an event with its options, variants, early-bird tiers,
|
||
forms, consent text, feedback form, cost template, allocation groups and
|
||
sessions (dates shifted to the new start). No registrations.
|
||
- **Series:** group separate events (Camp 2025, Camp 2026; monthly Men's
|
||
Breakfast) on one series page, with "Next in this series" and a series
|
||
follow/notify option.
|
||
- **Year-on-year reports:** registrations, income and attendance compared
|
||
across events in a series.
|
||
- **Season pass:** a bundle across events in a series (extends event bundles).
|
||
|
||
---
|
||
|
||
## 7. Apple and Google Wallet tickets
|
||
|
||
- "Add to Apple Wallet" / "Add to Google Wallet" buttons next to the PDF
|
||
ticket, in email and in My Events.
|
||
- Pass shows event, date (next session for multi-day), location, holder name,
|
||
QR code.
|
||
- Passes **update** when the event/session changes (time, venue, cancelled).
|
||
- Requires an Apple Developer account (pass signing certificate) and a Google
|
||
Wallet issuer account; both configured in Site settings and **optional** —
|
||
hidden when not configured.
|
||
|
||
---
|
||
|
||
## 8. Outbound webhooks
|
||
|
||
Let other systems react to what happens in Hope Events.
|
||
|
||
- Admin adds endpoints and picks events: `registration.created`,
|
||
`registration.paid`, `registration.cancelled`, `payment.refunded`,
|
||
`ticket.checked_in`, `user.created`, `event.published`…
|
||
- Payloads signed with HMAC (secret per endpoint), retries with back-off,
|
||
delivery log with "Resend".
|
||
- Enables integrations (ChurchSuite, Planning Center, Google Sheets via
|
||
Zapier/Make) without building each one. Native integrations can follow later.
|
||
|
||
---
|
||
|
||
## 9. Admin dashboard, command palette, bulk actions
|
||
|
||
- **Dashboard** as supervisors' home: today's events/sessions, check-ins so
|
||
far, money in (today/week), pending items (unreconciled payments, consent
|
||
outstanding, waitlist offers, data requests), alerts.
|
||
- **Command palette (Ctrl+K):** jump to any event, user, registration,
|
||
ticket or setting; quick actions ("New event", "Find payment R450").
|
||
- **Bulk actions** on registration/attendee lists: message, export, mark
|
||
paid, move option, cancel, allocate.
|
||
- **Saved filters** per user on list pages.
|
||
|
||
---
|
||
|
||
## 10. Languages (Afrikaans first)
|
||
|
||
- i18n framework on the frontend (e.g. `next-intl`); English + Afrikaans.
|
||
- User language preference; emails, WhatsApp messages and PDFs sent in the
|
||
user's language, with per-language templates.
|
||
- Event content stays as typed by the supervisor (optional translated title /
|
||
description fields later).
|
||
|
||
---
|
||
|
||
## 11. Accessibility pass (WCAG 2.2 AA)
|
||
|
||
- Audit public pages, registration, payment, My Events and the kiosk.
|
||
- Keyboard navigation, focus states, labels, colour contrast (light and
|
||
dark), screen-reader names for icon buttons, error messages linked to
|
||
fields.
|
||
- Automated `axe` checks in the Playwright suite so it doesn't regress.
|
||
|
||
---
|
||
|
||
## 12. Multi-campus (one install per church)
|
||
|
||
**Decision: one install per church.** Each customer gets their own install
|
||
and database (the `/setup` wizard already makes this possible). This gives
|
||
full data separation — which also helps the POPIA story — and lets each
|
||
church run on a different server. **True multi-tenant is out of scope.**
|
||
|
||
**Multi-campus** is in scope: one church with several campuses inside one
|
||
install.
|
||
|
||
### Setup
|
||
|
||
- Site settings → **Campuses**: off by default. When on, admins add campuses
|
||
(name, address, contact, optional logo/colour accent, default location for
|
||
events).
|
||
- Single-campus churches never see campus fields anywhere.
|
||
|
||
### How it works
|
||
|
||
- **Events** belong to one campus, or to **All campuses** (church-wide
|
||
conferences, camps).
|
||
- **Public site:** campus filter on the events list, remembered per visitor;
|
||
campus shown on event cards and tickets. Optional campus landing pages
|
||
(`/campus/<slug>`).
|
||
- **Users** can set a home campus (used as the default filter and for
|
||
broadcasts). Accounts stay shared across campuses — one login for the
|
||
church.
|
||
- **Campus-scoped roles:** a supervisor or staff member can be limited to
|
||
one or more campuses (sees and manages only those events, registrations,
|
||
scanning, cashups). Admins and unscoped supervisors see everything.
|
||
- **Reports, cashup, dashboard, funnel stats:** filter by campus plus a
|
||
church-wide total.
|
||
- **Broadcasts:** target "everyone at campus X".
|
||
- **Kids check-in rooms** and **volunteer roles** are per campus.
|
||
- **Notification recipients** per campus (e.g. the campus pastor gets that
|
||
campus's daily summary).
|
||
|
||
### Edge cases
|
||
|
||
- Turning campuses off with campus data present → hidden, not deleted.
|
||
- Deleting a campus with events → blocked; archive instead.
|
||
- Moving an event to another campus → scoped supervisors of the old campus
|
||
lose access; logged in the audit log.
|
||
|
||
### Data model (sketch)
|
||
|
||
```prisma
|
||
model Campus {
|
||
id String @id @default(uuid())
|
||
name String
|
||
slug String @unique
|
||
address String?
|
||
isActive Boolean @default(true)
|
||
order Int @default(0)
|
||
events Event[]
|
||
}
|
||
// Event.campusId String? (null = all campuses)
|
||
// User.homeCampusId String?
|
||
// UserCampusScope { userId, campusId } — empty = unscoped
|
||
```
|
||
|
||
### Running many installs
|
||
|
||
Because every church is a separate install, a few tools make that manageable:
|
||
|
||
- **Install script:** fresh server → Node, Postgres, PM2, reverse proxy,
|
||
`.env`, migrations, then `/setup` in the browser.
|
||
- **Update script:** pull a tagged version, back up the database, run
|
||
migrations, build, restart; roll back on failure.
|
||
- **Version and health:** current version shown in admin and on a `/health`
|
||
endpoint (version, DB, jobs, last backup), reported to the
|
||
[Hub](#13-central-management-platform-hope-events-hub).
|
||
|
||
---
|
||
|
||
## 13. Central management platform (Hope Events Hub)
|
||
|
||
A separate app, run by Joshua, to see and manage **every Hope Events
|
||
install** from one place: who the customers are, which version each runs,
|
||
whether they're healthy, basic usage stats, and their **licence** status.
|
||
|
||
**Who:** Joshua (and any future support staff). Customers never log in to it.
|
||
|
||
### Separate app, separate repo, separate server
|
||
|
||
- **Its own git repository:** `joshua/hope-events-hub` on the same Gitea
|
||
server (c-git.crosscode.co.za), with its own semver tags, CHANGELOG and
|
||
deploys. It is never shipped to churches, so none of its code (customers,
|
||
licence signing key, other churches' data) ends up on a customer's server.
|
||
- Same stack (Next.js + Express + Prisma + Postgres) so it's familiar.
|
||
- **What lives where:**
|
||
- Hub repo: dashboard, customers, licences, licence **signing** (private
|
||
key — only ever on the Hub server), `/api/installs/register` and
|
||
`/api/installs/heartbeat` endpoints, alerts.
|
||
- Hope Events repo: the Hub **client** — licence step in `/setup`,
|
||
"Connect to Hub" in Site settings, heartbeat job, licence verification
|
||
with the Hub's **public** key, read-only mode, banners.
|
||
- **The contract between them** (register / heartbeat payloads, licence token
|
||
format) is versioned with a `protocolVersion` field and documented in both
|
||
repos (`docs/hub-protocol.md`). The Hub accepts the current and previous
|
||
protocol version, so churches on older versions keep reporting while they
|
||
wait to update.
|
||
- Runs on its own server at **`events-hub.crosscode.co.za`**. If the Hub is
|
||
down, **no church install is affected**.
|
||
- **Licence keys are generated by the Hub:** Joshua clicks "Generate" on a
|
||
customer and the Hub creates the key (not typed by hand).
|
||
- Hope Family Church's own site is the first registered install.
|
||
|
||
### Registration (setup → Hub)
|
||
|
||
1. The `/setup` wizard gets a **Licence** step: the customer enters the
|
||
**licence key** Joshua issued (created in the Hub first).
|
||
2. When setup completes, the install sends `POST /api/installs/register`
|
||
with: licence key, site name, URL, admin contact, version, server info
|
||
(Node/Postgres version, OS), timezone, enabled features.
|
||
3. The Hub checks the key, links the install to the customer, and returns an
|
||
**install id + install secret** (stored encrypted in AppSettings) and a
|
||
**signed licence token**.
|
||
4. Existing installs (already past setup) get a **"Connect to Hub"** button
|
||
in Site settings → Licence.
|
||
|
||
All later calls are signed with the install secret (HMAC) and go over HTTPS.
|
||
|
||
### Heartbeat (install → Hub)
|
||
|
||
- Every 15 minutes (job queue) the install POSTs a heartbeat:
|
||
- **Health:** version, uptime, DB OK, job queue OK, last successful
|
||
backup, disk free, last error count (from Sentry or local logs), pending
|
||
migrations.
|
||
- **Basic stats (aggregate counts only):** users, active/upcoming events,
|
||
registrations and tickets (30 days / all time), check-ins, WhatsApp/email
|
||
sent, enabled payment providers, campuses on/off.
|
||
- **Income in ranges, never exact amounts:** total payments received (30
|
||
days / all time) reported as a band — R0–R1 000, R1 001–R5 000,
|
||
R5 001–R20 000, R20 001–R50 000, R50 001–R100 000, over R100 000. The
|
||
band is worked out on the install; the exact figure never leaves it.
|
||
- **Install → Hub only.** The install pushes; the Hub never needs to reach
|
||
into the install, so it works behind firewalls and the Hub has no access
|
||
to church data.
|
||
- **No personal information is ever sent** (no names, emails, phones,
|
||
attendee data). Listed in the install's POPIA operator register and
|
||
privacy notice as "aggregate usage statistics to the software provider".
|
||
|
||
### Hub dashboard
|
||
|
||
- **Installs list:** customer, URL, version (flag if behind the latest tag),
|
||
licence status, last heartbeat (red if > 1 h ago), health status.
|
||
- **Install detail:** health history, stats charts, version history,
|
||
licence history, notes, contacts.
|
||
- **Alerts** (email/WhatsApp to Joshua): install offline > 1 h, backup
|
||
failed or older than 48 h, disk low, errors spiking, licence expiring.
|
||
- **Releases:** latest version and changelog; installs see "Update
|
||
available" in their admin.
|
||
- **Customers:** contact details, plan, licence(s), billing notes.
|
||
|
||
### Licences
|
||
|
||
| State | What the install does |
|
||
|-------|----------------------|
|
||
| **Active** | Everything works |
|
||
| **Grace** (expired or Hub unreachable) | Everything works; admins see a banner "Licence needs renewing — X days left". Default 14 days |
|
||
| **Suspended** | **Read-only:** no new events, no new registrations or payments. Existing tickets **still scan**, and admins can still view and **export all data** |
|
||
| **Revoked** | Same as suspended, with a permanent banner |
|
||
|
||
- Joshua turns a licence on/off, sets expiry dates and plan in the Hub; the
|
||
install picks up the change on its next heartbeat.
|
||
- The licence is a **signed token** (Ed25519: the Hub signs with its private
|
||
key, installs only hold the public key). Installs verify it **offline**, so
|
||
a Hub outage never switches a church off, and a token can't be faked by
|
||
editing the database.
|
||
- **Never a hard kill:** a church must always be able to scan tickets for
|
||
events people already paid for, and get its own data out (POPIA access
|
||
rights). Suspension blocks *new* activity only.
|
||
- **One plan for now.** Plan limits (e.g. number of campuses, WhatsApp) can
|
||
be carried in the token later.
|
||
|
||
### Things to be aware of
|
||
|
||
- Code runs on the customer's server, so a determined developer could patch
|
||
out the licence check. Real protection is the **contract** plus updates and
|
||
support only being available to valid licences — the licence check keeps
|
||
honest customers honest.
|
||
- The Hub should not offer remote commands (run updates, impersonate,
|
||
database access) in v1: it's the most valuable target an attacker could
|
||
hit, so keeping it read-only toward installs keeps every church safe if it
|
||
is ever compromised. Remote "trigger update" can be revisited later.
|
||
- The Hub itself needs 2FA, backups and the same audit log approach.
|
||
|
||
### Data model (Hub, sketch)
|
||
|
||
```prisma
|
||
model Customer { id, name, contactName, email, phone, notes, licences, installs }
|
||
model Licence { id, customerId, key, plan, status, expiresAt, graceDays, issuedAt }
|
||
model Install { id, customerId, licenceId, name, url, secretHash, version,
|
||
lastHeartbeatAt, health Json, registeredAt }
|
||
model Heartbeat { id, installId, receivedAt, version, health Json, stats Json }
|
||
// rolled up to daily after 30 days
|
||
model InstallEvent { id, installId, type, message, createdAt } // alerts, version changes
|
||
```
|
||
|
||
---
|
||
|
||
## 14. Foundations (platform work)
|
||
|
||
Not visible to users, but this is what makes 2.0 a safe product to sell and
|
||
keep adding to.
|
||
|
||
| Item | What |
|
||
|------|------|
|
||
| CI (Gitea Actions) | Lint, typecheck, Jest, build on every push; deploy on tag |
|
||
| Playwright E2E | Register → pay (sandbox) → ticket → scan → cashup; multi-day check-in; kiosk |
|
||
| Job queue | Move `setInterval` jobs out of `index.js` into `pg-boss` (uses Postgres): retries, no double runs, job history in admin |
|
||
| Split big controllers | `registrationController` (2.3k lines), `eventController`, `paymentController` → services by domain. Done **as each feature touches them**, not as a big-bang rewrite |
|
||
| TypeScript backend | Incremental (`allowJs`), new code in TS first; share zod schemas/types with the frontend |
|
||
| API v2 | `/api/v2` with consistent errors, pagination and an OpenAPI spec (replaces the hand-written API docs). `/api` (v1) kept during the transition |
|
||
| Dependencies | Express 5, native `fetch` instead of `node-fetch`, password hashing upgrade with rehash-on-login |
|
||
|
||
---
|
||
|
||
## Suggested build order
|
||
|
||
| Phase | Version | Features | Why |
|
||
|-------|---------|----------|-----|
|
||
| 0 | 1.32.0 ✅ | **Early-bird ribbon** on event cards (shipped 2026-10-22) | Quick win |
|
||
| 1 | 1.33.0 ✅ | **Multi-day events — phase A** (sessions, whole-course tickets, late joining + late pricing, per-session check-in with whole-day window, attendance register, optional reminders, certificates) | Requested first; touches registration/tickets/scanning, so it shapes later work |
|
||
| 2 | 1.34.0 ✅ | CI + Playwright E2E + job queue | Safety net before the riskiest work (payments). Session reminders move to the queue |
|
||
| 3 | 1.35.0 ✅ | Multi-day events — phase B (pick sessions, per-session capacity) | Builds on phase A, now with E2E coverage |
|
||
| 4 | 1.36.0 ✅ – 1.37.2 | Payment provider interface (Yoco moved behind it) → PayFast, then hiding "Pay online" when it can't be used and payment status checks (SnapScan and Zapper dropped) | Payment code gets split out of the big controller as part of this |
|
||
| 5 | 1.38.0 ✅ 1.39.0 ✅ 1.40.0 ✅ | **POPIA toolkit** (1.38.0: user privacy centre, unsubscribe, WhatsApp STOP, data requests inbox; 1.39.0: privacy notice editor + versions, operator register, consent log, access log; 1.40.0: retention jobs, breach register, children’s-data flags, readiness page) | Main selling point; retention jobs use the job queue |
|
||
| 6 | 1.41.0 + Hub v1 | Install/update scripts, `/health`, licence step in `/setup`, "Connect to Hub", heartbeat, licence states — plus the **Hub** app (separate repo) | Must exist before the first church is sold; Hope Family Church registers as install #1 |
|
||
| 7 | 1.42.0 | **Multi-campus** | Kids rooms and volunteer roles are per campus, so campuses come first |
|
||
| 8 | 1.43.0 – 1.44.0 | Kids check-in, Volunteer rosters | Depend on sessions and campuses |
|
||
| 9 | 1.45.0 – 1.47.0 | Event series & cloning, Wallet passes, Outbound webhooks | Smaller, independent wins |
|
||
| 10 | 1.48.0 – 1.50.0 | Admin dashboard/Ctrl+K/bulk actions, Afrikaans, Accessibility pass | Polish before the big release |
|
||
| 11 | 1.51.0 | TypeScript backend + OpenAPI + `/api/v2` alongside v1 | The breaking-change groundwork |
|
||
| 12 | **2.0.0** | v1 API removed, migration guide, marketing site/docs for selling | The release |
|
||
|
||
Each phase should add matching help content (`frontend/src/content/help/`),
|
||
audit log entries, Jest tests, a CHANGELOG entry, and updates to FEATURES.md
|
||
and SETTINGS.md — same as the previous round. Branch → merge → tag → push per
|
||
feature.
|
||
|
||
---
|
||
|
||
## Decisions (2026-10-22)
|
||
|
||
| # | Question | Decision |
|
||
|---|----------|----------|
|
||
| 1 | Pick sessions in the first release? | No — whole-course first (1.33.0), pick sessions as a separate release (1.35.0) |
|
||
| 2 | Late joining and late price | Set **per event**: late joining on/off (until session N), and the late price (full / pro-rata / fixed) |
|
||
| 3 | Check-in window | Per event, **whole day** by default (any time on the session's date); "session time only" as the alternative |
|
||
| 4 | Certificates | Yes — four templates (Classic, Modern, Church, Kids); the event creator chooses whether to issue certificates and which template |
|
||
| 5 | Session reminders | Off by default, configurable per event |
|
||
| 6 | Selling model | **One install per church** — data separation, any server. No multi-tenant. **Multi-campus** inside an install is in scope |
|
||
| — | Pro-rata rounding | Nearest R10 |
|
||
| — | Multi-campus | In scope (§12), inside one install |
|
||
| — | Central management | **Hope Events Hub** in its **own git repo** (`joshua/hope-events-hub`): installs register on setup, send heartbeats (health, version, aggregate stats), licences switched on/off centrally |
|
||
| 7 | Payment provider order | Yoco · PayFast. Joshua can supply documentation links |
|
||
| — | SnapScan, Zapper | Dropped (2026-10-23): Yoco and PayFast are enough. Later versions moved up by two |
|
||
| — | Provider fees | Not deducted or estimated automatically; handled by people during cashup. No "pass fee to payer" |
|
||
| — | Minimum online payment | R15 for all providers |
|
||
| — | Scope | All features in this document approved; POPIA toolkit and kids check-in are highlights |
|
||
| — | Early-bird ribbon | Added before everything else; shipped as 1.32.0, all later versions moved up by one |
|
||
| — | Hub domain | `events-hub.crosscode.co.za` (2026-10-23) |
|
||
| — | Licence keys | Generated by the Hub when Joshua clicks "Generate" on a customer (2026-10-23) |
|
||
| — | Grace period / suspension | 14-day grace; suspended = read-only (tickets still scan, data export still works) (2026-10-23) |
|
||
| — | Income in the heartbeat | Included, as **ranges** only (R0–R1 000, R1 001–R5 000, …), never exact amounts (2026-10-23) |
|
||
| — | Licence plans | One plan for now (2026-10-23) |
|
||
| — | POPIA: message kinds | Bulk messages are either **Event updates** (to registrants — still sent after unsubscribing) or **News & announcements** (marketing — opt-in, one-click unsubscribe). Every bulk email links to the preferences (2026-10-23) |
|
||
| — | POPIA: existing users | Kept opted in to news (logged as "existing account"), asked once at their next login to confirm or opt out. New sign-ups choose with an unticked box (2026-10-23) |
|
||
| — | POPIA: deleting an account | A request: the account is deactivated at once, personal info is anonymised when an admin approves. Waits while the person has an open registration for an upcoming event (2026-10-23) |
|
||
| — | POPIA part 2 split | 1.39.0: privacy notice editor + versions + re-acceptance, operator register, Information Officer on the contact page, consent log, access log. 1.40.0: retention policies + nightly job, breach register, children's-data flags, readiness page. Hub moves to 1.41.0 (2026-10-23) |
|
||
| — | POPIA: privacy notice | Today's notice becomes the starting template, one editable box per section; org name, Information Officer, retention periods and the operator list fill in automatically. Publishing saves a numbered version (2026-10-23) |
|
||
| — | POPIA: re-accepting the notice | The admin decides per version ("significant change — ask everyone to accept again"); people are asked once after login (2026-10-23) |
|
||
| — | POPIA: inactive accounts | Anonymised after 5 years without a login or registration, **on by default**, with a warning email 30 days before; staff/supervisor/admin accounts never (2026-10-23) |
|
||
| — | POPIA: WhatsApp STOP | In 1.38.0, via WAWP's incoming `message` webhook with a secret in the URL (WAWP doesn't sign webhooks) (2026-10-23) |
|
||
|
||
## Still open
|
||
|
||
Nothing for now.
|