- 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
171 lines
13 KiB
Markdown
171 lines
13 KiB
Markdown
# 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](./ROADMAP-v2.md) (decisions table at the end); the previous round of
|
|
features is in [FEATURES-ROUND-1.md](./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.
|