- 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
13 KiB
Handover — where things stand and how to carry on
Last updated: 2026-10-23, after 1.40.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.
Done so far (road to 2.0)
| Version | What |
|---|---|
| 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
- 1.41.0 + Hub v1 — install/update tooling and the Hope Events Hub (separate repo
joshua/hope-events-hub).
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 →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. - 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. - CI:
.gitea/workflows/ci.ymlruns everything once a Gitea Actions runner is registered (none yet). Tag deploys need theDEPLOY_HOST,DEPLOY_USER,DEPLOY_SSH_KEY(+ optionalDEPLOY_PATH) secrets.
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.
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.