Files
hope-events/docs/HANDOVER.md
joshuaandClaude Opus 5.5 d265047c84 POPIA toolkit part 2: privacy notice editor, operators, consent and access logs
- Admin -> Privacy (POPIA): edit the privacy notice (built-in text as the
  start, placeholders for org/IO/operators), drafts, preview, numbered
  versions; earlier versions readable on /legal/privacy?version=N
- "Significant change" versions are accepted once after login
  (NoticePrompt); sign-up records the version accepted
- Operator register (seeded with the services the app uses), listed in
  the notice via {operators}
- Consent log (news opt-ins, notice acceptances, parental consent) and
  access log (report exports, registers, certificates, group lists, form
  answers, consent/medical lists, backups, impersonation)
- Information Officer on the contact page
- Unit tests (privacyNotice.test.js), e2e (privacy-notice.spec.ts); docs

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

12 KiB

Handover — where things stand and how to carry on

Last updated: 2026-10-23, after 1.39.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.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. 1.40.0 — POPIA toolkit part 3 (spec §3 in the roadmap, decisions table): retention policies per data category + nightly job (inactive accounts anonymised after 5 years, on by default, warning email 30 days before, never staff/supervisor/admin), breach register with notification templates, children's-data flags in reports, POPIA readiness page with a score. The notice's retention section should then fill in from the policies (a new placeholder).
  2. 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 with npm version X.Y.Z --no-git-tag-version in ., backend and frontend → add the ## [X.Y.Z] - date heading under [Unreleased] in CHANGELOG.md → git merge --no-ff into main → git tag vX.Y.Z → git push origin main <branch> vX.Y.Z. One minor version per feature.
  • Before tagging: run npx eslint src in frontend over the whole app (the deploy runs next 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 and git 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 /docs route list in backend/src/index.js, and in-app help in frontend/src/content/help/ (register new pages in registry.ts).
  • Prisma migrations: npx prisma migrate dev --create-only --name x against a local DB. Prisma 7 keeps adding an unrelated _EventNotifyRecipients primary-key/index change — delete those two blocks from the generated SQL. It also rewrites the comment in migration_lock.toml; git checkout that file back. New AdminAuditAction values 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 own ecosystem.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 unless E2E_PAYFAST_MERCHANT_ID, E2E_PAYFAST_MERCHANT_KEY and E2E_PAYFAST_PASSPHRASE are 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 public BACKEND_URL and PAYFAST_SKIP_IP_CHECK=true.
  • End-to-end: npm run test:e2e from the repo root (first time: npm install and npx 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 from backend/ with PORT=5100, the e2e DATABASE_URL, JOBS_ENABLED=false, TWO_FACTOR_REQUIRED_ROLES=false, and npx next dev -p 3100 from frontend/ with NEXT_DIST_DIR=.next-e2e NEXT_PUBLIC_API_URL=http://localhost:5100 — then run npx 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.local people only.
  • CI: .gitea/workflows/ci.yml runs everything once a Gitea Actions runner is registered (none yet). Tag deploys need the DEPLOY_HOST, DEPLOY_USER, DEPLOY_SSH_KEY (+ optional DEPLOY_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 in backend/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 the PaymentProvider interface (documented at the top), provider on/off and order (AppSetting payment_providers), the R15 minimum, and createCheckout. Tests: backend/tests/payments.test.js.
  • backend/src/payments/providers/yoco.js — Yoco: checkouts, webhook signature check, webhook parsing. Keys/webhook admin stays in utils/yoco.js + controllers/yocoController.js.
  • backend/src/payments/webhook.js — shared webhook processing (Payment with provider, 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-status job, 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>.js with the same interface, add it to PROVIDERS in index.js, mount handleProviderWebhook('<id>') at /api/webhooks/<id> (routes/webhookRoutes.js), and give it a settings card in frontend/src/components/admin/PaymentsTab.tsx. New providers start switched off.
  • Payment.method stays the wallet type (card, apple_pay…) because cashups bucket on it; the provider is Payment.provider.

Where the privacy (POPIA) code lives

  • backend/src/utils/privacy.js — news opt-in (setNewsPreferences, logged in CommunicationConsent), unsubscribe tokens (HMAC with JWT_SECRET, never expire), filterRecipients (every bulk send goes through it: kind 'update' passes, 'news' only to opted-in accounts and not to MessagingOptOut addresses), 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, and emailEventAttendees / whatsappEventAttendees in eventController take kind; scheduled payloads carry it.
  • WhatsApp STOP: handleIncomingMessage in whatsappController.js (WAWP message event on /api/whatsapp/webhook?secret=…; secret in AppSetting WAWP_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 anonymiseUser and exportUserData.
  • Privacy notice: utils/privacyNotice.js (built-in sections, placeholders, versions, draft in AppSetting privacy_notice_draft, noticePromptPending); public page app/legal/privacy (renders whatever /api/privacy/notice returns), admin app/dashboard/admin/privacy + components/admin/privacy/*, prompt components/shared/NoticePrompt.tsx (via PrivacyPrompts.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 in ACCESS_ACTIONS. Operators: Operator table, seeded in the 1.39.0 migration.
  • e2e: privacy-notice.spec.ts publishes a version that needs accepting, then accepts it for all seeded users in afterAll so 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.