# Handover — where things stand and how to carry on Last updated: 2026-10-23, after **1.37.4**. 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.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/`) 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.38.0–1.39.0 — POPIA toolkit** (spec §3 in the roadmap). 2. **1.40.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/` → 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 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 `_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/.js` with the same interface, add it to `PROVIDERS` in `index.js`, mount `handleProviderWebhook('')` at `/api/webhooks/` (`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`. ## Known gaps / follow-ups - 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.