A pass sells a ticket at each of several events in a series for one price. Supervisors set passes up on the Event series page (events and ticket per event; fixed once sold). Buying on the series page registers the buyer at every event at once with one checkout (free passes give tickets straight away; Pay now if online payment isn't possible). The price is split over the events in proportion to their normal prices: each registration line is priced at its share and the webhook records one Payment per event, so cashups stay per event. Pass registrations aren't repriced, take no coupons and can't be edited. Migration 20261028150000_season_pass. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
30 KiB
Handover — where things stand and how to carry on
Last updated: 2026-10-28, after 1.47.3 (and Hub 1.4.0). The plan itself is in ROADMAP-v2.md (decisions table at the end); the previous round of features is in FEATURES-ROUND-1.md.
The product is called Cross Events (renamed from Hope Events in 1.41.3); the Hub is the
Cross Events Hub. Technical names keep hope-events on purpose — the repo, browser storage
keys (hope_events_*), the settings encryption salt, applied migrations, PM2/nginx names, the
install folder and the database name — because renaming them would sign everyone out, make
encrypted settings unreadable or break existing installs.
Done so far (road to 2.0)
| Version | What |
|---|---|
| 1.47.3 | Season passes (§6 part 4): passes on the Event series page, buy on the series page (a registration per event, one checkout), price split by normal prices, one Payment per event from the webhook. Migration 20261028150000_season_pass |
| 1.47.2 | Series: year on year report (§6 part 3): GET /api/series/:id/compare (compareSeries in utils/series.js), report seriesComparison in ReportsV2, "Compare" on the Event series page |
| 1.47.1 | Event series (§6 part 2): Event series page, Series box on events, public /series/<slug>, "Next in this series", follow (News & announcements email from the series job when a new event goes live). Migration 20261028140000_event_series |
| 1.47.0 | Copy an event (§6 part 1): "Copy" opens the event editor filled in from the source (campus, sessions, options, tiers, sections, form, recipients, feedback, certificates); "New start" moves every date; allocation groups and (admins) costs copied by POST /api/events/:id/copy-from. No registrations, coupons, attachments, kids check-in or volunteer roles |
| 1.46.0 | Volunteer rosters (§5): serving roles per event or session, anyone signs up (or supervisors assign), day-before reminders, "can't make it" swaps that happen on accept (notify recipients emailed), self check-in with the event's QR code or by staff, kids-room roles count as leaders on duty, free volunteer ticket off by default, roster sheet and hours served. Also fixes the 1.45.0 kids ticket being offered publicly |
| 1.46.1 | Removed the Gitea CI workflow (no runner): test, lint and deploy by hand |
| 1.46.2 | Licence page: what's new (from WHATS-NEW.md via the Hub), withdraw update request, "Not now", urgent updates, provider notices banner (Hub 1.4.0: releases from git tags, Notices page) |
| 1.45.1 | Privacy notice (built-in text) and terms of use brought up to date with kids check-in, sessions, certificates, campuses, the Hub heartbeat and where data is stored. Installs with a published notice must publish a new version themselves |
| 1.45.0 | Kids check-in (§4), fully digital: families/children/authorised adults kept between events (My account → Kids), staff desk with pickup codes and a "write on the sticker" panel, rooms with age ranges and ratios, room screen (on duty, call parent, alerts), check-out by code (wrong code alerts, override with reason), offline desk with reserved codes, attendance through a free kids ticket; 6-month retention rule for children |
| 1.44.0 | Campus-scoped staff (§12 part 2): Admin → Users "Limit to campuses"; utils/campusScope.js on every staff/supervisor route (and owner-or-staff routes), lists filtered, scanner names the other campus, church-wide events view/scan/door-only for scoped staff; route test tests/campusScopeRoutes.test.js |
| 1.43.0 | Campuses (§12 part 1): Site settings → Campuses (off by default), events per campus or all campuses, public campus filter (remembered, starts on home campus) and /campus/<slug> pages, home campus on the profile, campus filters on Reports and the dashboards, "Everyone at a campus" in broadcasts, campus summary recipients, campus on ticket PDFs, heartbeat campusCount. Also adds the hub_update_requested audit action missing since 1.42.0 |
| 1.42.1 | Production Hub public key built in (publicKey.js); HUB_PUBLIC_KEY only needed for a local Hub |
| 1.42.0 | Request update instead of the deploy command: Site settings → Licence sends a signed update request to the Hub (protocol §3a), which opens an alert and emails Joshua; "Update requested on …" until the site runs that version, and the admin update banner hides meanwhile. Audit action hub_update_requested. Needs Hub 1.1.0 |
| 1.41.3 | Renamed Hope Events → Cross Events in all UI text and docs (technical identifiers unchanged, see above) |
| 1.41.2 | Background jobs page: last-run times were 2 h ahead (Prisma's pg adapter drops the offset of timestamptz in raw queries — read pg-boss times AT TIME ZONE 'UTC'); APP_TIMEZONE documented |
| 1.41.1 | Licence fixes — no licence (never connected, or the Hub answers INSTALL_NOT_FOUND, which now drops the old licence) is read-only like suspended; read-only also blocks editing events/options/variants; heartbeat 30 s after start and after connecting; LICENCE_CHECK_DISABLED for e2e |
| 1.41.0 | Cross Events Hub connection — licence step in /setup and Site settings → Licence, 15-minute heartbeat (health, totals, income bands), licence states checked offline (grace banner for site admins only, read-only when suspended/revoked), /health, "update available", scripts/install.sh. The Hub itself is joshua/events-hub 1.0.0 |
| 1.40.0 | POPIA toolkit part 3 — readiness checklist with score, retention policies + nightly retention job (inactive accounts anonymised after 5 years with a 30-day warning, on by default), breach register with notification drafts, children's/medical flag in the access log, {retention} in the notice |
| 1.39.0 | POPIA toolkit part 2 — Admin → Privacy: privacy notice editor (built-in text as the start, placeholders, drafts, numbered versions, re-acceptance after login), operator register, consent log, access log; Information Officer on the contact page |
| 1.38.0 | POPIA toolkit part 1 — Privacy page (news opt-in + history, download my data, correction and deletion requests), Event update vs News & announcements on every bulk send, unsubscribe links + one-click headers, WhatsApp STOP, one-time news question, Admin → Data requests, thorough anonymising |
| 1.32.0 | Early-bird ribbon on event cards |
| 1.33.0 | Multi-day events phase A — sessions, late joining + late pricing, per-session check-in, attendance register, session reminders, certificates (4 templates, verify page) |
| 1.34.0 | Background jobs on pg-boss (admin Background jobs page), Playwright e2e tests, Gitea CI workflow, scripts/deploy.sh |
| 1.35.0 | Multi-day events phase B — "pick sessions" (per-session tickets and prices, session capacity) |
| 1.37.4 | Deploy script: no tag = latest main; per-install ecosystem.config.js out of git (ecosystem.config.example.js) |
| 1.37.3 | Fix: the production build failed on unescaped quotes in a help text |
| 1.37.2 | Payment status checks (PayFast transaction history; job every 15 min; Payments → Reconcile → Waiting for payment), PayFast Test connection |
| 1.37.1 | "Pay online" hidden when no provider is on or under R15; SnapScan/Zapper dropped from the roadmap |
| 1.37.0 | PayFast (signed form, ITN checks), ProviderCheckout with amount checks, the "Pay online" page (/pay/<id>) where payers choose a provider |
| 1.36.0 | Payment provider interface (backend/src/payments) with Yoco behind it, ProviderTransaction, Payment.provider, Site settings → Payments (on/off and order per provider), R15 minimum |
| 1.35.1 | Waitlist per session, session picker at the door, "All sessions" tickets let people tick sessions at one price; fixes (per-session stock limits, offline service worker serving stale scripts) |
Next up
Hub 1.1.0 is deployed with update requests rolled out, and scripts/install.sh has been tried on
a test server (both done 2026-10-28).
- 1.48.0 — outbound webhooks (§8).
Working on this repo
- Release routine: branch
feat/<name>→ commit → bump withnpm version X.Y.Z --no-git-tag-versionin.,backendandfrontend→ add the## [X.Y.Z] - dateheading under[Unreleased]in CHANGELOG.md → write the church-facing## X.Y.Zsection in WHATS-NEW.md (plain language;— urgentin the heading for a security or serious fix) →git merge --no-ffintomain→git tag vX.Y.Z→git push origin main <branch> vX.Y.Z. One minor version per feature. - Before tagging: run
npx eslint srcinfrontendover the whole app (the deploy runsnext build, which fails on any lint error, e.g. an unescaped ' or " in JSX text). If a dev server is running, a full check build needs its own folder:NEXT_DIST_DIR=.next-buildcheck npx next build(then delete it andgit checkout frontend/tsconfig.json). - Changelog: add entries to
[Unreleased]as you go, in plain language. CHANGELOG.md is the full log; WHATS-NEW.md is what churches see before updating (the Hub reads it from main every 15 minutes, for tagged versions only and sets the latest version from the tags — nothing to do in the Hub). - Docs to keep in step: CHANGELOG.md, FEATURES.md, README.md, SETTINGS.md,
backend/API_DOCUMENTATION.md, the
/docsroute list inbackend/src/index.js, and in-app help infrontend/src/content/help/(register new pages inregistry.ts). - Prisma migrations:
npx prisma migrate dev --create-only --name xagainst a local DB. Prisma 7 keeps adding an unrelated_EventNotifyRecipientsprimary-key/index change — delete those two blocks from the generated SQL. It also rewrites the comment inmigration_lock.toml;git checkoutthat file back. NewAdminAuditActionvalues need a migration. - Server Node version: the deployment notes install Node 20, so dependencies must support Node 20 (that's why pg-boss is pinned to 10.x).
- Deploying:
bash scripts/deploy.sh [tag]on the server (no tag = latest main). Each install keeps its ownecosystem.config.js(PM2 app names, port), which is not in git. The script runs from a temp copy of itself and rolls back on failure before the PM2 reload. It was tested by running it against a throwaway clone with npm/pm2 stubbed: across the tracked → untracked change for the PM2 file, deploying main, failed builds and a bad tag. - Pushing: pushing from some shells fails with "Failed to authenticate user" (a stale saved credential); pushing from your own terminal works.
Tests
- Backend unit tests:
cd backend && npm test(Jest). - PayFast e2e (
e2e/payfast.spec.ts) pays into a real PayFast sandbox account and is skipped unlessE2E_PAYFAST_MERCHANT_ID,E2E_PAYFAST_MERCHANT_KEYandE2E_PAYFAST_PASSPHRASEare set (from the Sandbox dashboard). PayFast's documented public test merchant (10000100) no longer accepts payments. ITNs can't reach localhost; they're covered by unit tests (backend/tests/payments.test.js) — to try one for real, use a tunnel, a publicBACKEND_URLandPAYFAST_SKIP_IP_CHECK=true. - End-to-end:
npm run test:e2efrom the repo root (first time:npm installandnpx playwright install chromium). They start their own backend (5100) and frontend (3100) against<your DATABASE_URL database>_e2e, which is dropped and re-seeded every run (e2e/support/prepare-db.js). The DB user must be able to create databases. ~5 minutes in dev mode. Locally, Playwright reuses servers already running on 5100/3100. If the run times out waiting for the frontend (the home page returns 500 until the e2e backend is up), start them yourself —node e2e/support/prepare-db.js, then the backend frombackend/withPORT=5100, the e2eDATABASE_URL,JOBS_ENABLED=false,TWO_FACTOR_REQUIRED_ROLES=false, andnpx next dev -p 3100fromfrontend/withNEXT_DIST_DIR=.next-e2e NEXT_PUBLIC_API_URL=http://localhost:5100— then runnpx playwright test. Don't edit frontend files while it runs. - Careful with ad-hoc scripts against a dev/test database that has SMTP set up:
registrations and check-ins send real email/WhatsApp. Use made-up
@test.localpeople only. - No CI: the Gitea workflow was removed (no machine to run a Docker runner). Run the
backend tests, frontend eslint and e2e tests yourself before tagging, and deploy by
hand with
scripts/deploy.sh <tag>on the server.
Where the multi-day code lives
backend/src/utils/sessions.js— all session rules (dates, check-in window, late pricing, pick sessions, capacity, grouping); unit tests inbackend/tests/sessions.test.js.backend/src/utils/certificates.js,sessionReminders.js,controllers/attendanceController.js(register + certificates API).backend/src/jobs/index.js— the background job registry.- Frontend:
src/lib/sessions.ts,src/components/events/ScheduleEditor.tsx,SessionPicker.tsx,SessionSchedule.tsx,src/app/dashboard/supervisor/attendance/.
Where the payment code lives
backend/src/payments/index.js— the provider registry and thePaymentProviderinterface (documented at the top), provider on/off and order (AppSettingpayment_providers), the R15 minimum, andcreateCheckout. Tests:backend/tests/payments.test.js.backend/src/payments/providers/yoco.js— Yoco: checkouts, webhook signature check, webhook parsing. Keys/webhook admin stays inutils/yoco.js+controllers/yocoController.js.backend/src/payments/webhook.js— shared webhook processing (Payment withprovider, donation split, registration status, tickets, emails,ProviderTransaction).backend/src/payments/checkouts.js— registration checkouts (balance, part, + donation).backend/src/payments/status.js— status checks for notifications that never arrived (payment-statusjob, Payments → Reconcile → "Waiting for payment"). PayFast looks payments up in its transaction history (findPayments); Yoco's Checkout API documents no lookup, so Yoco checkouts are "can't check". Tests:backend/tests/paymentStatus.test.js.- Adding a provider: write
providers/<id>.jswith the same interface, add it toPROVIDERSinindex.js, mounthandleProviderWebhook('<id>')at/api/webhooks/<id>(routes/webhookRoutes.js), and give it a settings card infrontend/src/components/admin/PaymentsTab.tsx. New providers start switched off. Payment.methodstays the wallet type (card, apple_pay…) because cashups bucket on it; the provider isPayment.provider.
Where the privacy (POPIA) code lives
backend/src/utils/privacy.js— news opt-in (setNewsPreferences, logged inCommunicationConsent), unsubscribe tokens (HMAC withJWT_SECRET, never expire),filterRecipients(every bulk send goes through it:kind'update' passes, 'news' only to opted-in accounts and not toMessagingOptOutaddresses), email/WhatsApp footers, STOP handling, data requests,deletionBlockers,anonymiseUser,exportUserData+ PDF. Tests:backend/tests/privacy.test.js; e2e:e2e/privacy.spec.ts.controllers/privacyController.js+routes/privacyRoutes.js(/api/privacy).- Bulk sends:
broadcastController,whatsappBroadcastController, andemailEventAttendees/whatsappEventAttendeesineventControllertakekind; scheduled payloads carry it. - WhatsApp STOP:
handleIncomingMessageinwhatsappController.js(WAWPmessageevent on/api/whatsapp/webhook?secret=…; secret in AppSettingWAWP_WEBHOOK_SECRET). - Frontend:
app/dashboard/user/privacy,app/unsubscribe,app/dashboard/admin/data-requests,components/shared/NewsPrompt.tsx(dashboard layout),MessageKindPicker.tsx. - Anything new that stores personal data must be added to
anonymiseUserandexportUserData. - Privacy notice:
utils/privacyNotice.js(built-in sections, placeholders, versions, draft in AppSettingprivacy_notice_draft,noticePromptPending); public pageapp/legal/privacy(renders whatever/api/privacy/noticereturns), adminapp/dashboard/admin/privacy+components/admin/privacy/*, promptcomponents/shared/NoticePrompt.tsx(viaPrivacyPrompts.tsx: notice first, then the news question). Changing the built-in text changes the live notice only for installs that haven't published a version yet. - Access log:
utils/accessLog.js(logDataAccess(req, { action, what, eventId, records })). Any new bulk export or list of personal data should call it and get a label inACCESS_ACTIONS. Operators:Operatortable, seeded in the 1.39.0 migration. - Retention:
utils/retention.js(policies in AppSettingretention_policies,runRetention,preview,noticeLinesfor{retention}), jobretention(03:30). Anything new that stores personal data should get a retention rule (a new entry inPOLICIES+ runner). Inactive = latest ofUser.createdAt,lastLoginAt(set on login), registrations/event end, attendee rows, payments. Readiness:utils/readiness.js. Breaches:SecurityBreach. Admin panels:components/admin/privacy/{Readiness,Retention,Breaches}Panel.tsx. - e2e:
privacy-retention.spec.tsruns the retention job on seeded six-year-old accounts (prepare-db.js→retention);privacy-notice.spec.tspublishes a version that needs accepting, then accepts it for all seeded users inafterAllso later spec files don't meet the prompt.
Where the Hub client lives (1.41.0, update requests 1.42.0)
- Protocol:
docs/hub-protocol.md— the same file is in the Hub repo (joshua/events-hub, developed at../events-hub); change both together. backend/src/hub/:publicKey.js(the production Hub key, built in since 1.42.1 — if the Hub's signing key is ever regenerated, update it here in a release;HUB_PUBLIC_KEYenv overrides it for a local Hub),licence.js(verify tokens, work out active / grace / suspended / revoked / unlicensed, cached 30 s),client.js(connect, heartbeat, update request — the last two sharesignedPost; AppSettinghub_*keys, which the generic settings endpoints never touch),report.js(health, totals, income bands),readOnly.js(the routes blocked in read-only mode — add any new "create" or event-edit route there). Read-only = suspended, revoked or unlicensed.- Job
hub-heartbeat(every 15 min), routes/api/hub/*(status, connect, heartbeat, update-request),/health. - Frontend:
components/admin/LicenceTab.tsx(Site settings → Licence, with the Request update button; itsConnectFormis also on the last/setupscreen),components/shared/LicenceBanner.tsx(site admins only). - Updates: sites never update themselves and never show the deploy command. "Request update"
tells the Hub (an
update_requestedalert + one email to Joshua); the Hub resolves it when the site's heartbeat reports a newer version. Joshua then runsscripts/deploy.shon the server. - Local testing: run the Hub (
../events-hub, ports 5200/3200), create a licence there, and start Cross Events withHUB_URL=http://localhost:5200andHUB_PUBLIC_KEY= the key on Hub → Settings. scripts/install.sh— fresh Ubuntu server to a running site (tried on a test server 2026-10-28).
Where campuses live (1.43.0)
backend/src/utils/campuses.js: the setting (campuses_enabled, off by default),campusWhere(list filters: one campus,'all'= church-wide only,withAllCampusesfor the public list),campusForEvent(what to save on an event),campusAudienceWhere(broadcast audience). Pure parts are tested intests/campuses.test.js.controllers/campusController.js, routes/api/campuses/*. The setting is changed only throughPUT /api/campuses/settings(it's inPRIVATE_KEYSin the settings controller).- While campuses are off, every campus field and filter is ignored (the data stays). New code
should check
campusesEnabled()the same way. - Frontend:
hooks/useCampuses.ts(one cachedGET /api/campusesper page load),components/shared/CampusFilter.tsx(CampusFilter,matchesCampus,CampusBadge),CampusAudiencePicker.tsx,components/admin/CampusesTab.tsx,components/events/EventsGrid.tsx(public filter,hope_events_campusin localStorage),app/campus/[slug]/page.tsx. - e2e: the campuses and two events' campuses are seeded in
e2e/support/prepare-db.jswith campuses off (so a pre-built frontend already has them);campuses.spec.tsturns them on and off.
Campus-scoped staff (1.44.0) — adding a staff route
- Every route behind
stafforsupervisormust carry a marker fromutils/campusScope.js, ortests/campusScopeRoutes.test.jsfails:scopeEvent({ resolve, from, need, ownerOk, nameCampus })— one event, or a record traced to one (RESOLVERS: registration, ticket, ticketQr, payment, eventOption, optionVariant, bundle, allocationSet/Group, waitlistEntry, scanLock, syncReport, section, certificate, consentRequest).need: 'manage'for setup, cashup and refunds (church-wide events refused).scopeBody([[field, resolver], …], { need, orphan })— records named in the body (lists of ids too);orphan: 'deny'when there must be an event (payments).scopeList— the controller filters withlistWhere(req, path)/withScope(where, extra)(never spread a scopewhereover one that has its own OR) or checks itself.unscopedOnly,campusIndependent('reason'),scopeBroadcast({ freeformField }).
- Routes open to the owner and staff (no role middleware, so the test can't see them) need
scopeEvent({ …, ownerOk: true })by hand — registrations, payments, tickets, consent requests and coupon validation have it. - Rules: admins never scoped; scopes ignored while campuses are off; another campus → 404, except
the scanner (403 naming the campus). Tested in
tests/campusScope.test.jsand e2ecampus-scoped.spec.ts(staff and supervisor scoped to E2E South; a seeded North ticket).
Where kids check-in lives (1.45.0)
backend/src/utils/kids.js— everything: codes (randomCode,normaliseCode), occasions (occasionFor: today's session, or the day), room suggestion and status, families and children (allergies/medical/custody encrypted withutils/encryption.js),checkIn(code per family per occasion viaKidsPickupCode, a free place onEvent.kidsOptionIdthroughticketForChild, aTicketUsageper check-in),checkOut/overrideCheckOut, messages,board, duty, offline snapshot andsyncOffline, and the privacy parts (removeFromFamily,exportFamily,runKidsRetention). Pure parts tested intests/kids.test.js.controllers/kidsController.js+routes/kidsRoutes.js(/api/kids); campus resolverskidsCheckInandkidsAlertinutils/campusScope.js.- Rooms are
AllocationGroups ofEvent.kidsRoomSetId(withminAge,maxAge,ratio). Event ids onEvent(kidsOptionId,kidsRoomSetId) are plain strings, checked in code. - Kids check-in isn't blocked in licence read-only mode, on purpose: it's like scanning tickets, and children's safety at pickup comes first.
- Frontend:
lib/kids.ts(types, room suggestion, offline copy inhope_events_kids_offline_<eventId>),components/kids/*(KidsEventPicker,ChildForm,RoomBoard,StickerPanel), pagesapp/dashboard/staff/kids(desk),staff/kids/rooms,supervisor/kids(setup),user/kids. - e2e:
kids.spec.tson the seeded hidden "E2E Kids Church" (set up, parent adds a child, desk checks in, parent sees the code, wrong code then the right one, room alert).
Where volunteer rosters live (1.46.0)
backend/src/utils/volunteers.js— everything: shifts (shiftOf: the role's own times, else its session's, else the event's), overlap and check-in-window rules, roles, sign-up / leave / swaps, the free volunteer place (givePlace/releasePlace,VolunteerSlot.registrationId), QR and manual check-in (kids-room roles start aKidsRoomDutywithfromRoster),rosteredForRooms(used by the kids board), messages, thevolunteersreminder job, hours served, and the privacy parts (exportVolunteering,forgetVolunteer). Pure parts tested intests/volunteers.test.js.reservedOptionIds/publicOptionsthere: the kids and volunteer tickets are left out of public event pages and refused on public registration (eventController,registrationController). A new staff-only ticket type should be added toreservedOptionIds.controllers/volunteerController.js+routes/volunteerRoutes.js(/api/volunteers); campus resolversservingRoleandvolunteerSlot; sign-ups, new roles and assigning are inreadOnly.js.- Swaps: no approval — accepting moves the slot at once (claimed with an
updateManyonstatus: 'open'so two people can't both take it);tellOrganisersemailsgetEventNotifyEmails(event)(notify recipients, else the creator), not the registrations inbox. - The check-in QR holds
SERVE-<Event.volunteerCheckInToken>; the page encodes it as a URL (/serve/check-in?code=…) so phone cameras open it directly. - Frontend:
lib/volunteers.ts,components/volunteers/{RosterSheet,CheckInQr}.tsx, pagesdashboard/supervisor/volunteers(roles, QR, ticket, hours),dashboard/staff/volunteers(the day),dashboard/user/serving(?event=<id>opens one event — the shareable sign-up link),serve/check-in. - e2e:
volunteers.spec.tson the seeded hidden "E2E Serving Sunday" (role, ticket hidden publicly, sign-up, cover request, staff take over, QR check-in, roster).
Where copying an event lives (1.47.0)
- The copy is made in the browser:
EventModalinapp/dashboard/supervisor/events/page.tsxtakessource(create mode,cloning), fills its state from it (cloneOptionsleaves out the kids and volunteer tickets and all ids; cancelled sessions dropped; sessions keyed by the source's ids so late joining still points right; sections loaded and mapped onto the copied options; feedback questions without the sent/answered state), andmoveAllDatesshifts every date for "New start". It saves through the normal create path, so a new field on the event form is copied automatically if it's initialised frombase. - What the form doesn't hold is copied by the server afterwards:
utils/eventClone.js(copyEventSetup: allocation sets/groups exceptkidsRoomSetId, costs for admins only since costs are admin-only) viaPOST /api/events/:id/copy-from. Tests:tests/eventClone.test.js, e2e "copy an event" insupervisor.spec.ts.
Where event series live (1.47.1)
backend/src/utils/series.js— input cleaning,seriesForEvent,nextInSeries(next public event starting later),splitEvents, follow/unfollow, theseriesjob (runAnnounceJob: public, not started,seriesAnnouncedAtnull → claimed, then followers filtered withfilterRecipientsas news),exportFollows. Tests:tests/series.test.js; e2eseries.spec.ts.controllers/seriesController.js+routes/seriesRoutes.js(/api/series, campus-independent: the list filters each series' events by scope). Event create/update takeseriesId; changing it clearsseriesAnnouncedAtso the new series' followers hear.SeriesFollowis personal data: inanonymiseUserandexportUserData. No retention rule of its own — follows go when the account is anonymised.- Frontend:
app/dashboard/supervisor/series,app/series/[slug],components/events/SeriesLinks.tsx,FollowSeriesButton.tsx(asks the API itself: the page is cached for everyone), the Series box in the event editor. Series pages aren't in the sitemap yet (no public list endpoint).
Where season passes live (1.47.3)
backend/src/utils/seasonPass.js—splitCents/itemShares(the price over the events by normal price, whole cents),notOnSale,passView,cleanPassInput,buyPass(advisory lock per pass, stock checked per item, a registration per event with a tranche at the share),startPassCheckout(checkout metadata{ type: 'season_pass', purchaseId }),recordPassPayment(called frompayments/webhook.jsrecordSucceededPayment;allocatepays each registration what it owes; first Payment carriesexternalId),notOnSeasonPass(route guard on options edit and coupons),exportPurchases. Tests:tests/seasonPass.test.js; e2e inseries.spec.ts.refreshPricingForRegistrationskips pass registrations;payments/status.jstreats a pass checkout as paid once its purchase is.- Frontend:
components/series/SeasonPassesPanel.tsx(Event series page),components/series/SeasonPassOffers.tsx(series page: buy, "Your passes", Pay now). - Not built yet: buying a pass at the door or by staff, cancelling a whole purchase in one go (staff cancel and refund each event's registration), one combined payment email (each event's payment sends its own).
Known gaps / follow-ups
-
WhatsApp STOP only works once someone pastes the webhook URL from Site settings → WhatsApp into WAWP (events
message+session.status), and only for senders WhatsApp identifies by phone number — a sender known only by a "LID" is logged and ignored (they can still use the link in the message). -
/api/webhooks/whatsapp(webhookController.handleWhatsappWebhook) is an old empty stub; the real WAWP webhook is/api/whatsapp/webhook. Remove the stub in a later release. -
There's no log of individual messages sent, so "Download my data" can't include "messages sent to you" yet.
-
The audit-log page's action filter list was already missing newer actions before 1.38.0 (only the three privacy ones were added).
-
At the door, per-session tickets are booked on new registrations only; editing an existing registration leaves its booked sessions as they are (the backend refuses changes to picked lines). "Add & charge" for a session at the scanner isn't built.
-
Ticket scanning by camera isn't covered by the e2e tests (headless browsers have no camera); scans are tested through the API.
-
Kids check-in: the offline desk is tested at the API level (sync, idempotency, code reservation), not in the browser. A family's two parents share one family; linking a second parent is done at the desk ("parents" endpoint) — there's no invite from My account yet.
-
Volunteer rosters: if a volunteer edits their free volunteer registration on the edit page, the volunteer ticket isn't among the public options, so saving drops it (the backend only refuses adding or increasing it). Rare; the place is still in the roster, and a supervisor can give a ticket at the door. Reminders are a single "day before" one (no per-event timing yet).