# Changelog All notable changes to this project are documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project follows [Semantic Versioning](https://semver.org/). ## [Unreleased] ## [1.35.0] - 2026-10-23 ### Added - **Pick sessions.** A multi-day event can let people choose which sessions to come to — day passes for a conference, or a weekly drop-in class. In the event wizard choose "How people book: Pick sessions", then in Items & Pricing set a ticket to "Pick sessions (price per session)". People tick the sessions they want when they register; each session is its own line and its own ticket, at the per-session price (early-bird and staff prices apply per session; late-joiner pricing doesn't). A session can be booked until it starts (staff can book until it ends). Whole-course tickets can still be sold alongside, and stop selling when the course starts. - **Session capacity**: in pick-sessions events each session can have a capacity, covering picked places plus whole-course tickets. Full sessions can't be ticked. A per-session ticket's stock limit is places per session. - Picked-session tickets only scan on their own session ("This ticket is for Class 2 (2026-11-11)…"), on and offline. The attendance register shows one row per person with a dash for sessions they didn't book, and completion and certificates count the sessions they booked. Session reminders go only to the people booked on that session. Tickets, printouts and the ticket email show the session's date. - The session picker is on the public registration page, Manual registration and the self-service kiosk. - The roadmap to 2.0 and a handover note are now in the repo: `docs/ROADMAP-v2.md`, `docs/HANDOVER.md` (plus the previous round's spec, `docs/FEATURES-ROUND-1.md`). ### Changed - Per-session tickets can't be changed from My Events' "edit booking" (book more sessions by registering again); they still count towards the total. At the door, per-session tickets are booked from Manual registration (the page says so). ## [1.34.0] - 2026-10-22 ### Added - **Background jobs page** (Admin → Site Settings → Background jobs): every scheduled job with its schedule, next run and last 10 runs (start, finish, result or error), and a "Run now" button (recorded in the audit log). - **End-to-end tests** with Playwright (`npm run test:e2e`): they start their own backend and frontend against a separate, freshly seeded database and cover the public pages, free and paid registration through payment, ticket, check-in and cashup, multi-day check-in with the attendance register and certificates, creating a multi-session event in the wizard, the self-service kiosk, and the jobs page. - **CI workflow** (`.gitea/workflows/ci.yml`): backend tests, frontend lint, typecheck and build, and the end-to-end tests on every push and pull request. It runs once a Gitea Actions runner is registered. - **Deploy script** (`scripts/deploy.sh `): backs up the database, checks out the tag, installs, migrates, builds and reloads PM2, going back to the previous version if a step fails before the reload. CI runs it over SSH for `v*` tags once the `DEPLOY_*` secrets are set. ### Changed - Timed background jobs (daily summary, waitlist, feedback, session reminders and certificates, consent, analytics rollup, backup, temp cleanup) now run on a job queue in Postgres (pg-boss) instead of in-process timers. Each runs once per schedule even with more than one server process, failed runs are retried, and a slow run never overlaps the next. Schedules are cron, in `APP_TIMEZONE` (Africa/Johannesburg by default) rather than the server's clock; change one with `JOB_SCHEDULE_`, or turn all jobs off with `JOBS_ENABLED=false`. The existing `*_ENABLED` flags still work; the old `*_JOB_INTERVAL_MS` settings are replaced by the schedules. If the queue can't start, the jobs run on timers as before. - The server lets a running job finish before it stops or restarts. ### Fixed - At the door, the Check-In tab counted every past session's check-in on a multi-day ticket, so from the second session the ticket showed as fully used. It now counts only today's session. ## [1.33.0] - 2026-10-22 ### Added - **Multi-day events.** An event can run over several sessions, e.g. every Wednesday 18:30–20:30 for 5 weeks. In the event wizard choose "Multiple sessions", create the dates from a repeat pattern (daily or weekly, every N, on chosen weekdays, for a number of sessions or until a date), then edit any session's label, times or location, add one-off dates, or cancel a session (kept for history). People register and pay once and get one ticket for every session. - The event page shows the schedule (past sessions greyed out, the next one highlighted, cancelled ones struck through). Event cards show "5 sessions · Wed 4 Nov – Wed 2 Dec", and multi-day events stay listed until their last session, sorted by their next one. Calendar files have one entry per session. Ticket PDFs, printed tickets and the ticket email give the session range. - **Late joining**: a multi-day event closes registration when its first session starts, unless late joining is on (until session N, set per event). Late joiners pay full price, a pro-rata price for the sessions still to come (rounded to the nearest R10, at least R10), or a fixed late price — main tickets only, and never more than the normal price. The event and registration pages say when the late price applies. - **Per-session check-in**: a multi-day ticket is checked in once per session. The scanner uses today's session (staff can pick another), shows the session on the confirm screen, and says "Already checked in for Week 3 at 18:42" on a second scan. Each event chooses whether check-in is open the whole day of a session (the default) or only from 2 hours before it until it ends. Offline mode downloads the list for today's session, and synced scans count for the session at their scan time. - **Session reminders**: optional per event — 1 hour, 3 hours and/or 1 day before each session, by email and/or WhatsApp (each person's notification preference still applies). Off by default. - **Attendance register** (Dashboard → Attendance): attendees × sessions with ticks, totals per session, completion, search and filters (completed, not completed, missed the last two sessions), and Excel/PDF export. Supervisors can tick someone present or absent, recorded in the audit log. Also works for single-date events (one "Checked in" column). - **Completion certificates**: optional per event, in the wizard's new Attendance step. Four designs (Classic, Modern, Church, Kids) with the site logo and colour, your title and wording ({name}, {event}, {sessionsAttended}, {totalSessions}, {endDate}), an optional signature name, role and image, and who qualifies (every session, at least N sessions, or at least N%). "Preview certificate" shows a sample. Certificates are issued automatically when the event ends (and can be sent automatically), or from the attendance register: issue now, issue to one person anyway, send, download all as one PDF, or revoke. People download theirs under My Certificates on My Events. Each has a code and QR code that anyone can check on the public page `/certificates/verify/`, which shows only the name, event and dates. - Saving an event where sessions moved or were cancelled offers a "Message attendees" link, since registrants aren't told automatically. - A background job every 5 minutes sends due session reminders and issues certificates (`SESSION_JOBS_ENABLED=false` to turn it off). The church's timezone for "today" defaults to Africa/Johannesburg (`APP_TIMEZONE` to change). ### Changed - The event wizard has a ninth step, Attendance, for late joining, check-in window, session reminders and certificates. ## [1.32.0] - 2026-10-22 ### Added - Event cards on the home page and events list show a diagonal "Early bird" ribbon in the top-right corner while the event has an early-bird tier running (deadline not passed and places left on that tier). It isn't shown when the event is sold out, registration is closed or not open yet, or the event is contact-only. ## [1.31.1] - 2026-10-22 ### Added - Settings → Organisation has a "Google Search Console verification" field. Paste the HTML tag from Search Console and the site outputs it on every page, so each deployment can verify its own domain without uploading a file. ## [1.31.0] - 2026-10-22 ### Added - The registration funnel's traffic source works without tagged links: visits from Facebook, Instagram, Google, X, LinkedIn and other sites are recognised from where the visitor came from (and Facebook's/Google's click ids). Links without a tag used to count as "direct". WhatsApp still only shows from tagged links (`?utm_source=whatsapp`), since it doesn't pass that on. - The event page's Share button shares the link tagged `?utm_source=share`, and Save QR encodes it tagged `?utm_source=qr`, so those visits show separately. - Event pages tell search engines about the event in Google's event format (schema.org `Event`): name, dates, venue, picture, organiser, ticket prices in Rand and whether it's sold out. Google can then show events with their date, venue and ticket link in search results and in its event listings. Contact-only events are listed without tickets. ### Changed - Event pages, the events list and the home page tell search engines their one official address, so `?utm_source=…`, `?src=…` and alias links all count towards the same page in search rankings. Shared event links also carry the event's address. ## [1.30.1] - 2026-10-22 ### Changed - All tickets now share the design of the tickets printed from the user dashboard: rounded white cards with a light border and soft shadow, the date beside the event name, and the QR code in the middle. This covers the PDF sent by email and WhatsApp (now 6 per page, so the QR codes are bigger) and printing from Event Tickets (staff) and At the door (supervisor). Printed names are now escaped, so a name with `<` or `&` prints correctly. ## [1.30.0] - 2026-10-22 ### Added - The register form asks for an optional **Donation amount** under the ticket options. Pick a suggested amount (worked out from the ticket total), enter a custom amount (min R15), or choose None. The donation shows in the total and is paid in the same Yoco checkout as the tickets. It's recorded as a separate donation to the event, so it never counts towards the registration's balance. ### Fixed - Event pages with a waitlist crashed with "Attempted to call soldOutLines() from the server". The sold-out helper now lives in `lib/waitlist.ts` so the server page can call it. ## [1.29.1] - 2026-10-21 ### Changed - The Coupon codes page matches the Bundle deals page and works on phones. Codes show as cards with the discount, events, uses, restrictions, dates and limits. On phones, the edit form and the list of uses open full-width from the bottom, and the discount type is picked from three buttons. - The coupons list hides codes that are off, expired or used up unless you tick "Show codes that are off, expired or used up". Search also matches event names. - When creating or editing a code, the Events list shows upcoming events only, soonest first, with a search box and a "Show past events" option. Past events already on the code stay listed so they can be unticked. ## [1.29.0] - 2026-10-21 Run `npm run prisma:deploy` in the backend when deploying. This release adds a migration. ### Added - Bundle deals now come in four types, chosen when you create one: - **Exact set**: these tickets in these numbers for one price, e.g. 2 adults + 3 kids for R220. This is how bundles worked before. - **Pick any**: any mix of the qualifying tickets for a set price, e.g. any 4 of Adult or Child for R250, so 2 + 2 and 4 + 0 both count. - **Buy X get Y free**: in every set of qualifying tickets the cheapest are free, e.g. buy 3, get 1 free. - **Group discount**: from a group size up, every qualifying ticket costs less (a price per ticket or a percentage off), e.g. 10+ tickets: R70 each. It applies once, to the whole group, and never makes a ticket dearer. - The deals list and the "Complete bundle" cards on the register form, at the door, manual registration and the kiosk describe each type in one line, e.g. "Any 4 of Adult, Child for R250". For a group discount the button reads "Complete group" and adds the tickets still needed. - `npm run set-demo`: Family Fun Day also has a pick-any deal, "Lunch for four" (any 4 lunch packs for R220). ## [1.28.3] - 2026-10-21 ### Fixed - Bundle deals edit form: each ticket row's quantity box filled the whole row, which pushed the ticket picker off the side and added a sideways scrollbar. The quantity box is narrow again, with the ticket picker beside it. ## [1.28.2] - 2026-10-21 ### Changed - The Bundle deals page has a new layout that works on phones. It shows upcoming events only, soonest first, with a "Show past events" option. Bundles show as cards with their tickets, price, saving, uses and dates. On a phone, the edit form opens full-width from the bottom, and its price section shows the standard price, the bundle price and the saving side by side. ## [1.28.1] - 2026-10-21 ### Added - At the door, manual registration and the self-service kiosk now list the event's bundle deals and show the same bundle cards as the register form: once a ticket from a bundle is picked, **Complete bundle** tops up the ticket numbers, and **+** adds another full set. ### Fixed - At the door now has the same "← Back to dashboard" link as the other dashboard pages. - At the door's "less than already paid" warning shows the total after bundle deals. ## [1.28.0] - 2026-10-21 Run `npm run prisma:deploy` in the backend when deploying. This release adds a migration. ### Added - **Bundle deals**: sell a set of tickets together for one price, e.g. 5 Adult + 3 Child for R500 instead of R550. Supervisors set them up per event under Dashboard → **Bundle deals**: the tickets and quantities (one variant or any variant), the price, optional dates, and on/off. - Bundles apply automatically, with no code, as many times as a registration's tickets allow; leftover tickets are charged as usual. A bundle only applies when it's cheaper than the tickets at the prices actually charged, early-bird and staff prices included. When several compete, the one that saves the most is used first. This works for online registration, manual registration, at the door and on the kiosk. - The event page and the register form list the event's deals, and every basket shows the saving live. On the register form, once a ticket from a bundle is picked, the bundle appears with what's missing and a **Complete bundle** button that tops up the ticket numbers. Once it's complete, **+** adds another full set. Invoices, receipts, emails, WhatsApp messages and the Yoco payment page show it as its own line, e.g. "Bundle: Family deal ×2". - The Discounts report lists bundle deals next to coupon codes. Each bundle shows how often it's been used and the total saving given. - `npm run set-demo`: Family Fun Day sells a **Family deal** bundle (2 adults + 3 kids for R220) in place of the old Family pass ticket. ### Changed - Coupon codes now work out after bundle deals, on what's left. Registration.discountTotal now holds bundle and coupon discounts together, with the bundle part in the new `bundleDiscount`. - Removing a code from a registration keeps its bundle deal. When an early-bird tier expires before payment and prices change, bundle deals are worked out again and coupon amounts are kept. - Registrations keep a bundle they already have, even after it's turned off or its dates pass. Deleting a bundle that has been used turns it off instead. ## [1.27.1] - 2026-10-21 ### Added - `npm run set-demo` also stages ten completed events from the past year, with door sales recorded by staff, staff cash counts, check-ins, refunds, donations and post-event feedback. Their cashups go through every path: a quick close; full closes with card fees, a cash deposit fee, a written-off shortfall, cash kept on hand and an unaccounted amount; a close, reopen and re-close; pending EFT money that was later received and some still outstanding; and one event left open with the cash counted, ready for a live cashup. Funnel stats older than 90 days are stored as daily totals, the way the app rolls them up. See `backend/DEMO_DATA.md`. ## [1.27.0] - 2026-10-21 Run `npm run prisma:deploy` in the backend when deploying. This release adds a migration. ### Added - Cashups now record where each payment method's money ended up. For cash, card, EFT and other, the expected takings are split into what's **in the bank**, **cash on hand**, and **pending** (on the card machine or Yoco report, or with a proof of payment), with a reference for the pending amount. Card fees, bank charges, cash shortfalls and other losses are **written off** with a reason. Each method shows what's still **unaccounted**. Closing with an unaccounted amount is allowed after a warning, and the amount comes off profit, as variance did before. - When staff have counted the cash, cash on hand is worked out for you: the count, less what reached the bank and any deposit fee written off as a bank charge. - Pending money on a closed event can be marked **received** or **written off** later from the cashup screen, without reopening the event. Each one is logged with who did it and when. - Reopening an event starts the next cashup from the last one, with any pending money settled since folded in. - Write-offs show as **Bank & card charges** on the cashup screen and in the Cashup, Finance and Profit reports. The Cashup report has new In bank, On hand, Pending, Written off and Unaccounted columns. ### Changed - Net profit now subtracts write-offs as their own line instead of showing card fees and bank charges as missing income. The net figure is the same. - Every dashboard page uses the same back link: the small "← Back to dashboard" link above the page title, as on the Waitlist page. The grey Back buttons are gone. Site settings, profile, donate and manual registration, which had no way back, now have one too. ## [1.26.1] - 2026-10-21 ### Added - `npm run set-demo` in the backend replaces the database contents with demo data for showing the platform to other churches. It keeps the site settings and the admin account, and stages six upcoming events with registrations, payments, tickets, a waitlist, camp cabins, coupons, costs and funnel stats. See `backend/DEMO_DATA.md`. ## [1.26.0] - 2026-10-21 ### Changed - Upgraded Prisma from 5.22 to 7.10. Prisma 7 connects through the `@prisma/adapter-pg` driver adapter (node-postgres) instead of the Rust query engine. The database URL moved from `schema.prisma` to the new `backend/prisma.config.js`, which loads `backend/.env` itself. All Prisma clients are now created in `src/config/db.js`, and the server shares one client instead of opening two connection pools. No schema or migration changes. - Prisma 7 checks TLS certificates on database connections. A `DATABASE_URL` that uses SSL with a self-signed or otherwise invalid certificate now fails to connect where Prisma 5 accepted it. - `npm run dev` in the backend now uses Node's built-in `node --watch` instead of `nodemon`. - The frontend now passes ESLint with no errors or warnings, down from 1,137, and `next build` runs ESLint again. It was skipped before (`eslint.ignoreDuringBuilds`), which is how the issues built up. - API data is now typed. `src/types` describes the records the backend returns, modelled on the Prisma schema, and `apiFetch` returns `unknown` unless the caller names a type, so untyped responses no longer pass silently. - Effects list everything they depend on. Data loaders are wrapped in `useCallback`, so a page reloads when its inputs change instead of showing stale data. - The pricing helpers that were copied into several pages now live in `src/lib/pricing.ts`. ### Fixed - Internal links on the home page, the 404 page and "Forgot your password?" now navigate client-side instead of reloading the whole page. - Changing the page size on Admin → Users fetched the list twice; it now fetches once. - At the door, the refund history now shows a refund's reason. The page was reading a field that doesn't exist, so the reason never appeared. - In the Master Orders report, a ticket option named like one of the fixed columns (e.g. "name") could overwrite that column's value. - Removed code that could never run: an unreachable "Pay with Yoco" button on the registration form, an unused attendee-forms block on the registration success page, unused export helpers in Reports, and fonts that were downloaded but never applied. ### Security - Backend `npm audit` now reports 0 vulnerabilities: - Patched `follow-redirects` (pulled in by `axios`) to 1.16.1, which fixes leaking credential headers on cross-origin redirects. - Removed `nodemon`, the only backend package that pulled in `braces`. - Jest's coverage loader now uses `js-yaml` 4, which drops the `sprintf-js` advisory. - Frontend: overrode `postcss-selector-parser` to 7.1.7, used by the Tailwind 3 build. The generated CSS is byte-identical. The remaining frontend findings (7 high) are all the same `braces` advisory (GHSA-vfj7-8cjw-p6xm). It has no patched release, comes in only through build and lint tools (Tailwind 3 and `eslint-config-next`), and only ever handles glob patterns from our own config, never user input. ## [1.25.1] - 2026-10-20 ### Fixed - On mobile, the cookie consent banner now sits above the bottom navigation bar instead of being hidden behind it. - The floating help button moves up above the cookie consent banner while it is showing, so the banner no longer covers it. ## [1.25.0] - 2026-10-20 ### Added - A **Yoco** tab in Site Settings for online payments: - **Secret key:** shown masked like a card number (`sk_test_***…aba`) with a Test/Live badge. Yoco checks a new key before it is saved. - **Webhook secret:** **Generate** registers this server's webhook URL (`BACKEND_URL/api/webhooks/yoco`, editable) with Yoco and saves the secret Yoco returns. **Enter manually** takes the secret of a webhook you registered yourself. - **Registered webhooks:** every webhook on the Yoco account, with its URL and mode. It shows which one is active and warns when a URL isn't this server. Each can be deleted; deleting the active one also removes its stored secret. - New admin endpoints under `/api/yoco` (config and webhooks). Changes are written to the admin audit log, without the secret values. ### Changed - The Yoco secret key and webhook secret are now stored in the database, AES-256-GCM encrypted, instead of only in `backend/.env`. `YOCO_SECRET_KEY` and `YOCO_WEBHOOK_SECRET` still work as fallbacks when the database has no value. `npm run migrate:env-to-db` moves them across. - Yoco secrets are never returned by `/api/settings/all` and can't be changed through `PUT /api/settings`. - Starting a checkout or sponsorship without a Yoco key now gives a clear "online payments are not configured" message instead of failing at Yoco. ## [1.24.0] - 2026-10-19 ### Changed - Ticket transfers now need the new holder to accept. - "Transfer" (in My Events, or At the door → Tickets) sends the person a link by email and/or WhatsApp, to the details typed. - Nothing changes on the ticket until they accept on the new `/transfer/` page. They can also decline. - On accepting, the transfer completes as before: new QR code, ticket delivery, account activation link, parental consent on youth events. - My Events shows "Waiting for … to accept" with a Cancel button. - Offers expire after 7 days or when the event starts, and a new offer replaces an open one. - Privacy for group tickets and transfers: there is no user search, and nothing reveals whether an email or phone number belongs to an account. - Registration responses no longer include named attendees' account links. - If an email and phone point at two different accounts, this is no longer reported to the person typing them. The email's account is used. - Terms of Use and Privacy Policy updated for everything added since 1.12: - booking for other people, ticket transfers, and youth-event consent with health information and how long it is kept; - who can see what (door staff, group leaders, supervisors), and read-only admin support access; - sponsorships, waitlists, discount codes and feedback; - two-factor authentication and offline scanning. - The Privacy Policy has a full **cookie policy**: every item the site stores in the browser (necessary, staff-only and optional), the optional visit statistics, and the third-party cookies from the Google Maps embed and Yoco checkout. - A new "Cookie settings" link in the footer (and a button in the policy) lets visitors change their choice. - The API docs page (`/docs`) now lists every mounted route. 45 were missing, including dashboard stats, backups, attachments, the audit log, ticket transfer links and WhatsApp admin. ### Fixed - After activating an account, the visitor is now really signed in. The token was saved under a key the app never read, so the "auto-login" didn't work. ### Notes - `npm install` succeeds in the root, `backend` and `frontend` with no missing packages. - The remaining `npm audit` findings are all in development/build tools (Jest, nodemon, ESLint, Tailwind's build), and none affect the running site. - `braces` and `sprintf-js` have no patched release. - `postcss-selector-parser` 7 would break Tailwind 3. - Requires the `add_transfer_requests` database migration. ## [1.23.0] - 2026-10-19 ### Added - Impersonate user (Admin → Manage users → Impersonate): an admin can see the site exactly as a user does, to help with problems such as "my ticket isn't showing". - A reason is required, and the session lasts at most 30 minutes. - A bright "Viewing as [name]" banner with a countdown and "End impersonation" shows on every page. - Impersonation is strictly read-only. - A server-side guard, mounted before every route, refuses any change (registrations, payments, refunds, cancellations, profile, consent, messages and everything else) made with an impersonation token. - Viewing pages doesn't trigger the price refresh it normally does. - Admins can't be impersonated, and the admin must have two-factor authentication on. - Impersonation is recorded in the audit log: start, end, reason, and every page and API call, including blocked attempts. - The audit log page lists recent sessions with their full trail. - New setting: email users when an admin views their account (Site Settings → Legal → Support access; off by default). - New `/api/admin/impersonate` endpoints. - Requires the `add_impersonation` database migration. ## [1.22.0] - 2026-10-19 ### Added - Registration funnel stats (first-party, no third-party analytics), showing where people drop off between seeing an event and paying. - A new cookie notice asks visitors once. Only those who choose "Allow" are counted, using a random anonymous visit id. - We record the event page view and the start of registration. The submit and paid stages come from the registration itself. - Only the stage, the traffic source and the device type are stored. No name, IP address or User-Agent is kept. - New report, **Registration funnel**: counts and conversion % per stage over a date range, a daily trend, and breakdowns by traffic source, device and ticket option. - Selecting several events compares them. - Sources come from `utm_source` links and from short links, which now add `?src=` when redirecting. - A "Conversion (past month)" tile on the supervisor and admin dashboards covers live events. - Raw visit records are rolled up into daily totals after 90 days by a daily job (`ANALYTICS_ROLLUP_ENABLED=false` to turn it off). - The privacy policy's cookies section now describes these statistics. - New `/api/analytics` endpoints. - Requires the `add_funnel_analytics` database migration. ## [1.21.0] - 2026-10-19 ### Added - Post-event feedback, set up in a new Feedback step in the event wizard. - Questions use the form builder. - You pick when it's sent (the default is the morning after the event) and who gets it: everyone who checked in, or everyone registered. - Each person gets a personal link by email (and WhatsApp where they use it). - Anyone who hasn't answered gets one reminder 3 days later. - Answers can be anonymous or named. - "Send now" is available, and the questions lock once people start answering. - New form field types: **Rating (1–5 stars)** and **Multiple choice**. They're used for feedback, and can also go on attendee forms (registration page, My Events, the kiosk, manual registration and Supervisor → Forms). - New report, **Feedback summary**: - response rate; - average rating per question, with star distribution; - multiple-choice and yes/no breakdowns as bar charts; - a list of text answers (with names unless the form is anonymous). - Select several events to compare their ratings (e.g. Camp 2025 vs Camp 2026). Exports to PDF and Excel like other reports. - New `/api/feedback` endpoints and a job every 15 minutes (`FEEDBACK_JOB_ENABLED=false` to turn it off). - Requires the `add_feedback_forms` database migration. ## [1.20.0] - 2026-10-19 ### Added - Sponsor a spot: a new event setting, "Allow sponsorships" (event wizard → Sponsor a spot). - It adds a "Sponsor a spot" button on the event page so people can fund a place for someone who can't afford it. - Sponsors choose one or more whole spots (at the main ticket price) or a custom amount, and pay through Yoco. Guests just give a name and email. - Sponsors can give anonymously, or add a name and a message for the organisers. - An optional goal ("12 of 20 spots sponsored") and public progress bar. - Each sponsorship is recorded as a donation tagged to the event, in that event's sponsorship pool. - Supervisors assign pool money to registrations with the existing donation assign/unassign flow, fully or partly. - Under Payments → Donations, sponsorships are labelled, there's an "Only the sponsorship pool" filter, and the sponsor's message is shown. - Who receives help is never shown to sponsors. - Sponsors get a "Thank you for sponsoring a spot" receipt. - Automations has a new "Thank Your Sponsors" email for events with sponsorships. - Email attendees can target an event's sponsors (`filter.audience: 'sponsors'`). - The Donations breakdown report has sponsorship pool columns per event: received, assigned and remaining. - New `/api/sponsorships` endpoints. - Requires the `add_sponsorships` database migration. ## [1.19.0] - 2026-10-19 ### Added - Rooms & groups (Supervisor → Rooms & groups), for camps and lockdowns: put attendees into rooms, cabins, small groups, teams or buses. - Each event can have several allocation sets (e.g. "Rooms" and "Buses"). Each set has groups with an optional capacity, an optional gender restriction and an optional leader (a staff member). - A drag-and-drop board puts people not yet placed on the left and groups, with capacity counters, on the right. Full groups and groups for the other gender won't accept a person. - Auto-allocate fills groups evenly with everyone not yet placed, respecting capacity and gender. It can keep people on the same booking together or spread them out. - Printable lists per group (with leader) and a master list. On youth events they can include medical and emergency details from consent forms; viewing those is recorded in the audit log. - "Show on tickets" prints the group on each person's ticket and shows it in My Events. - Email and WhatsApp attendees can be sent to one group ("Bus 2 leaves at 07:30"). Messages go to whoever booked for the people in it. - Group leaders see their people under Staff → My groups. Medical details are shown to supervisors only. - New event setting "Ask each person's gender" (under Named attendees). It adds a gender choice for each named person on the registration page, at the door, on the kiosk, in manual registration and in My Events, and is needed for gender-split rooms. - New `/api/allocations` endpoints. - Requires the `add_room_allocation` database migration. ## [1.18.0] - 2026-10-19 ### Added - Ticket transfer. On My Events, "Transfer" hands a ticket to someone else before the event: enter their name and email or phone. - The new holder gets the ticket by email/WhatsApp with a new QR code. If they're new, an inactive account is created and they get an activation link. - The old QR code stops working, so screenshots of it won't scan. Scanning it says who the ticket was transferred to, and offline-mode sync flags it. - For a ticket covering several people, one place is split off to the new holder. - The payer stays responsible for the registration. - Transfer rules: - Only before the event starts, and only for tickets that haven't been scanned. - It can be turned off per event (new "Ticket transfer" setting in the event wizard, on by default). Supervisors can always transfer, from At the door → Tickets. - On youth events, the new holder's date of birth, and a parent's details for a minor, are needed, and a new consent request is sent. - The previous holder's attendee form answers and consent medical details are removed. - Every transfer is recorded in the audit log. - New endpoints `POST /api/tickets/:id/transfer` and `GET /api/tickets/:id/transfers`. - Requires the `add_ticket_transfer` database migration. ## [1.17.0] - 2026-10-19 ### Added - Offline ticket scanning. While online, staff pick an event and section on the scanner and tap "Enable offline mode". The section (or the whole event) is locked to their device and its tickets are downloaded to the device. - With no connection, scans are checked against that list and queued on the device. - The panel shows when the list was downloaded and how many scans are waiting. - Queued scans sync automatically when the device is back online (or with "Sync now"), and check-in messages go out then. - "Release section" ends offline mode and deletes the list from the device. - Only one device can hold a section at a time (a whole-event lock clashes with every section). While a section is locked, other devices see who holds it and can't scan it, online or offline, so a ticket can never be redeemed twice by two offline devices. - Syncing is idempotent and never redeems more than is left on a ticket. Anything that needs a person to look goes into a sync report: tickets not found, cancelled registrations, used-up tickets, scans outside the locked section, and check-ins without consent. - Supervisor → Offline scanning page: active locks with force release (for a lost or flat phone; its scans still sync if it reconnects, and are flagged), and sync reports to review. Locks, releases, force-releases and syncs are recorded in the audit log. - The scanner is installable as an app (web app manifest), and a service worker caches the scanner page and its scripts so it opens with no connection. - The app now stays signed in with the last confirmed profile when it's reloaded without a connection, instead of sending the user to the login page. - New `/api/scan-locks` endpoints. - Requires the `add_offline_scanning` database migration. ### Changed - `POST /api/tickets/scan/:qrCode` now accepts `sectionId` and checks it on the server; this was previously only checked in the browser. - It now refuses (423) scans in a section that another device has locked for offline scanning. ## [1.16.0] - 2026-10-19 ### Added - Waitlist for sold-out tickets. A new event setting (event wizard → Visibility) turns it on, with how long offered places are held (24 hours by default) and whether someone can be offered fewer places than they asked for. - Sold-out tickets show "Join waitlist" on the event page, the register page and the events list. Guests can join on events that allow guest registration. - When places free up, the next person is emailed/WhatsApped a claim link, and the places are held for them so nobody else can buy them. Places free up through a cancellation (by the person or staff), a smaller booking, or a raised stock limit. - The claim link opens the register page with the places pre-selected, and works without logging in. - Unclaimed offers expire (checked every 10 minutes) and move on to the next person. - People can leave a waitlist from My Events, which now lists their waitlists and any places held for them. - Supervisor → Waitlist page: the queue per event, with offer now (even over the stock limit), move up/down and remove. - The daily summary email shows each event's waitlist count. - New `/api/waitlist` endpoints. `POST /api/registrations` accepts `waitlistToken`. - Requires the `add_waitlist` database migration. ### Fixed - Manual registrations (Manual registration, At the door, the kiosk) and adding places to an existing booking never checked stock, so either could take an option over its stock limit. Both are now checked like a normal registration. - Stock is now counted in one place (`utils/stock.js`) for registration checks, `availableCount` and `isSoldOut`, including places held for waitlist offers. ## [1.15.0] - 2026-10-19 ### Added - Parental consent for youth events. A new "Requires parental consent" event setting (event wizard → Visibility) has its own consent text and optional medical info, emergency contact and photo consent questions. It turns on named attendees automatically. - Registering then needs a name and date of birth for every main-ticket place. Anyone under 18 on the event's start date needs a parent or guardian's name, email and phone. - This applies on the registration page, manual registration, at the door, the kiosk, and when editing names in My Events. - The parent gets an email and a WhatsApp (always both) with a link to a new consent page (`/consent/`). There they read the text, answer the optional questions, type their full name and confirm they're the parent or guardian, then give or decline. - The system records the time, IP, device, signed name, and the exact consent text and version shown. - Medical and emergency details are stored encrypted. - The registrant and the event's notification recipients are told the outcome. - Tickets are still issued, but the scanner blocks check-in until consent is given, showing why. Supervisors can override, and the override is recorded in the audit log. On a youth event, a ticket with no named attendee counts as missing consent. - A daily 09:00 job reminds unanswered requests after 2 days, then every 3 days until the event. It also deletes medical and emergency details a set number of days after the event (Site Settings → Legal, default 30). `CONSENT_JOBS_ENABLED=false` turns it off. - Supervisor → Parental consent page: consent status per attendee, resend one or all pending requests, re-request consent after the consent text changes, and a printable, confidential medical & emergency list. Every view of the list is recorded in the audit log. - New reports: "Parental consent status" and "Medical & emergency list". - My Events shows each attendee's consent status, with a "Resend consent" button. - Anonymising or closing an account also clears that person's attendees' parent, date-of-birth, medical and emergency details. - New endpoints under `/api/consent`. `GET /api/tickets/scan-preview/:qrCode` now returns `consentStatus`, and `POST /api/tickets/scan/:qrCode` accepts `overrideConsent`. - Requires the `add_parental_consent` database migration. ## [1.14.0] - 2026-10-19 ### Added - Group tickets (named attendees). A new "Named attendees" event setting (event wizard → Visibility) lets someone registering several people give each main-ticket place a name, with an optional email and phone. It's offered on the registration page, Manual registration, At the door and the self-service kiosk. - Each named person gets their own single-person ticket, with their name on the ticket PDF, the scanner's confirm screen and the staff ticket list. Places left unnamed share one quantity ticket as before. - The payer chooses whether every ticket comes to them or each person gets their own by email/WhatsApp; anyone without contact details falls back to the payer. - The payer stays the account owner for billing, invoices and payments. - An attendee whose email or phone matches an existing account is linked to it (never when the email and phone belong to two different accounts). Their ticket then appears in their own My Events, marked "Booked by …", and they can open and submit their own attendee form. - My Events → registration popup has a new "People" section for adding or changing names and contact details, switching ticket delivery, and sending an attendee a link to fill in their own form (`/forms?registrationId=…&attendeeId=…`). An attendee whose ticket has been scanned can't be removed. - Attendee forms now show one block per named person, labelled with their name, and each response is saved against that attendee. - New endpoints `PUT /api/registrations/:id/attendees` and `POST /api/registrations/:id/attendees/:attendeeId/send-form-link`. `POST /api/registrations` and `/manual` accept `attendees` and `ticketDelivery`. - Requires the `add_named_attendees` database migration. ### Changed - Reducing the quantity of a main ticket removes the newest names beyond the new quantity. - Check-in confirmations for a named ticket go to that person when the registration sends each person their own tickets. - Unauthenticated guest registration now matches an existing account by phone as well as email, and is refused (409) when the email and phone belong to two different accounts, instead of silently reusing whichever account the email matched. - Free registrations now issue tickets through the same generator as paid ones, instead of a separate copy of the logic. ### Security - Registration details (`GET /api/registrations/:id`) hide attendees' email addresses and phone numbers from anyone other than the payer and staff, such as an attendee opening their form link. ## [1.13.0] - 2026-10-19 ### Added - Coupon codes. Supervisors create percentage, fixed-amount or free ("comp") codes under Supervisor → Coupon codes. - Codes can be limited to particular events, ticket options or variants, a date window, total and per-person uses (one per person by default), a minimum number of tickets, and specific people, roles (e.g. Staff and above) or an email domain. Restricted codes need a logged-in account, so they can't be shared around. - By default the better of the code and the early-bird price wins; a code can instead stack on top of early-bird pricing. - People enter codes under "Have a code?" on the registration page and in My Events (for registrations not yet paid in full). Staff can apply them in Manual registration, At the door and the self-service kiosk. - The discount is locked in when it's applied, the same way early-bird prices are. It appears as its own line on invoices, receipts, registration and payment emails, WhatsApp messages and the Yoco checkout. - Editing a registration's items recalculates percentage and free codes; fixed-amount codes keep their amount. Cancelling a registration frees the use. A free code that covers everything marks the registration paid and sends the tickets. - Use limits are enforced under a database lock, so the last available use can't be claimed twice. - Staff price: a ticket option or variant can carry a staff price (event wizard → Items & pricing) that Staff and above pay automatically when it's lower than the normal price. - New "Discounts (coupon codes)" report (Reports → Financial), showing uses and value given away per code and event. The Finance report's "What was sold" is now shown after discounts. - Coupon activity in the admin audit log: codes created, edited and turned off, and codes applied or removed by staff. - New endpoints: `POST /api/coupons/validate`, `GET|POST /api/coupons`, `GET|PUT /api/coupons/:id`, `GET /api/coupons/redemptions`, `POST|DELETE /api/registrations/:id/coupon`. `POST /api/registrations` and `/manual` accept `couponCode`. - Registration responses now include `discountTotal` and `subtotalComputed`. - Requires the `add_coupon_codes` database migration (`npm run prisma:deploy` in `backend`). ### Fixed - Yoco checkout line items priced each item at its most recent price times the whole quantity. When a line spanned several price tranches (some tickets at early-bird, some at full price), the item lines didn't match the amount charged. They now list each price actually charged. - The free-registration check on `POST /api/registrations` and `/manual` had the same fault: it used the latest price for the whole quantity instead of the real total. ## [1.12.0] - 2026-10-19 ### Added - Two-factor authentication is now required for Admin and Supervisor accounts. An admin or supervisor without it is sent a code at their next login (by email, or WhatsApp when SMTP isn't set up), and entering it turns two-factor on — the same code step as before, with a note explaining why. Trusted devices don't skip this first code. They can change whether codes come by email or WhatsApp from Profile & Security, but can no longer turn two-factor off (`POST /api/users/2fa/disable` returns 403 for them). - Existing admin and supervisor sessions without two-factor are ended on their next request (`401` with `code: "TWO_FACTOR_REQUIRED"`), and the login page explains why. Promoting a user to supervisor or admin also ends their sessions if they don't have two-factor yet, so they enrol at their next login. An admin "Reset 2FA" for a supervisor/admin now means they set it up again at their next login. - The requirement is skipped when no channel can deliver a code — no SMTP configured and no WhatsApp — so a fresh install (or the setup wizard, which also gets a short grace period) never locks the first admin out. `TWO_FACTOR_REQUIRED_ROLES=false` turns the requirement off as an emergency switch. - `FEATURES.md`, a repo-root overview of everything the platform does — roles, public site, events, registration, payments, tickets, messaging, dashboards, cashup, reports, settings, security, background jobs, and a page map. ### Changed - `info.md` (the overview for prospective churches) brought up to date with features added since it was written — donations, invoices and receipts, cashup and costs, profit reporting, audit trail, scheduled messaging, self-service kiosk, scan points, backups, branding, and privacy controls. ## [1.11.0] - 2026-10-08 ### Added - Optional two-factor authentication. Any user can turn it on from Profile & Security and choose to receive a 6-digit code by email or WhatsApp; the chosen channel is confirmed with a code before it takes effect. At login the code is asked for after the password, expires after 10 minutes and allows 5 attempts, with a "Resend code" option. If a WhatsApp code can't be sent, it falls back to the account's email address. - "Trust this device for 30 days" on the code step, so a remembered device skips the code. Trusted devices are forgotten when the password is changed or reset, when sessions are signed out everywhere, and when two-factor is turned off. - Admins can reset two-factor authentication for a user who can no longer receive codes ("Reset 2FA" on the Users page). Turning two-factor on or off shows in Account activity. - The self-service kiosk asks supervisors with two-factor on for their code at setup. Changing the event now re-confirms the password against the existing kiosk session (`POST /api/users/verify-password`) instead of logging in again. ### Changed - `POST /api/users/login` returns `{ twoFactorRequired, challengeId, method, destination }` instead of a token for accounts with two-factor on; the token is issued by the new `POST /api/users/login/2fa`. Accounts without two-factor are unaffected. Requires the `add_two_factor` database migration (`npm run prisma:deploy` in `backend`). ## [1.10.6] - 2026-10-08 ### Fixed - The frontend production build (`next build`) no longer hangs or fails when the backend isn't reachable. With no `NEXT_PUBLIC_API_URL`/`API_URL` set, the root layout fetched a relative `/api/settings` URL that never settled during prerender, so every page timed out after 60 seconds; server-side fetches now fail fast when no API base is configured. The home page and the Events page also no longer fail the build when `/api/events` can't be reached — they build with an empty list and fill in on the first revalidation. At runtime a failed fetch still errors, so the last good page keeps being served. ### Security - Updated dependencies to clear `npm audit` advisories: `express`, `multer`, `proxy-addr`, `qs` and others in the backend, `next` (15.5.27) and `sharp` (0.35.5) in the frontend, and `nodemailer` from 9 to 10 (requires Node.js 20 or newer). The root package now overrides `shell-quote` to a patched release, since every `concurrently` version pins a vulnerable one. Backend production dependencies now report no known vulnerabilities; the remaining advisories are in development tooling (`nodemon`, `jest`, `tailwindcss` 3) and have no non-breaking fix. - Dependency install scripts are now explicitly allow-listed via `allowScripts` in the backend and frontend `package.json` (Prisma, `@sentry/cli`, `unrs-resolver`), so npm no longer blocks them. ## [1.10.5] - 2026-10-01 ### Fixed - Events that have already started but not yet finished now show on the home page and the Events page. Both pages were hiding an event as soon as its start time passed; they now keep it listed until its end time, which matches what `GET /api/events` already returns. ## [1.10.4] - 2026-09-01 ### Fixed - "Add to calendar" (`GET /api/events/:id/ics`) always failed with a 400 in production: the `ics` library's `url` field requires a scheme (`https://...`), but `FRONTEND_URL`/`APP_BASE_URL` was configured without one, so `createEvent()` rejected it before generating any output. A missing scheme is now assumed to be `https://` instead of failing. ## [1.10.3] - 2026-08-28 ### Fixed - Creating an event was never recorded in the admin audit log — 1.10.0 added `event_created` as a logged action on the frontend's filter list, but the backend's `createEvent` never actually called `logAdminAction`, only `updateEvent`/`deleteEvent` did. Event creation is now logged the same way. - Staff cancelling a registration via `PUT /api/registrations/:id` (the staff status-change endpoint, separate from the owner-facing `DELETE /:id` cancel route) wasn't logged at all — only the `DELETE` path logged `registration_cancelled`. Both paths now log it, verified against all six audit categories promised in 1.10.0 (refunds, donation assign/unassign, manual registrations, staff-initiated cancellations, event create/update/delete, settings changes) with no other gaps found. - Added a "Back to dashboard" link to Admin → Audit log, matching the back-link pattern already used on the Cashup page. ## [1.10.2] - 2026-08-28 ### Fixed - Sentry wasn't instrumenting Express (`[Sentry] express is not instrumented` at startup): `express`, `cors`, and `@prisma/client` were required at the top of `backend/src/index.js` before `Sentry.init()` ran, but Sentry's auto-instrumentation patches those modules via a require hook that only works if `Sentry.init()` runs first. `Sentry.init()` now runs immediately after `dotenv.config()`, before any of the libraries it instruments are required. ## [1.10.1] - 2026-08-28 ### Security - Fixed a path-traversal vulnerability in event-image, branding (logo/favicon), and event-attachment uploads: the stored filename embedded the client-supplied `originalname` unsanitized, so a crafted filename (e.g. containing `../`) could write the uploaded file outside the intended `public/uploads` subfolder anywhere the server process could write. Uploaded files are now always saved under a server-generated random name; the original filename is preserved only as display metadata. - `express-rate-limit` was declared as a root-only dependency despite being required directly by the backend (`backend/src/index.js`) — a plain `cd backend && npm install`, as documented in the deployment guide, would not have installed it. It's now a proper `backend/package.json` dependency. - Bumped `next` (frontend) off a version affected by a critical unauthenticated RCE in the React Flight protocol (GHSA-9qr9-h5gf-34mp) and several other CVEs, and switched it from an exact pin to `^15.5.24` so future patch releases install automatically. - Bumped `multer`, `nodemailer`, `jsonwebtoken`, and `uuid` (backend) to versions fixing DoS, SMTP/CRLF-injection, HMAC-verification, and buffer-bounds advisories; added an `overrides` entry so the vulnerable `uuid` nested under `exceljs` is also patched. Ran `npm audit fix` across all three workspaces (root/backend/frontend) — 0 known vulnerabilities remain. - `jwt.verify()` now pins `algorithms: ['HS256']` explicitly rather than trusting the algorithm from the token header. - Uploaded assets served from `/uploads` now get `Content-Security-Policy: default-src 'none'; sandbox` and `X-Content-Type-Options: nosniff`, so an uploaded SVG containing a `