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

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

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. 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.
  • Retention: utils/retention.js (policies in AppSetting retention_policies, runRetention, preview, noticeLines for {retention}), job retention (03:30). Anything new that stores personal data should get a retention rule (a new entry in POLICIES + runner). Inactive = latest of User.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.ts runs the retention job on seeded six-year-old accounts (prepare-db.js → retention); 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.