Assigning a donation to a registration was one-way: the refund flow could reverse the money but left the donation's "leg" payment in place, permanently locking that portion of the donation as used even though it had been refunded back out. Adds POST /api/payments/unassign-donation, which deletes the leg, reverts the registration's status/tickets the same way a refund downgrade already does, and notifies the registrant. New "Assigned donations" list on the supervisor Payments page surfaces existing legs with an Unassign action, since no such list existed before. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Hope Events Platform
Hope Events is a full-stack event management platform that allows users to browse events, register for them, make payments, and manage their tickets.
Project Structure
The project is divided into two main parts:
- Backend: Node.js + Express + Prisma + PostgreSQL
- Frontend: Next.js (React) + Tailwind CSS
Backend
The backend provides a RESTful API for managing users, events, registrations, payments, uploads, and tickets. It includes:
- JWT-based authentication and role-based authorization
- Event and registration management
- Payment integration with Yoco (checkout + webhooks)
- Ticket generation (PDF + QR) and email delivery
- Static uploads for event images
Documentation: see Backend API Documentation and Backend README for environment setup and endpoints.
- Backend API Documentation: ./backend/API_DOCUMENTATION.md
- Backend README: ./backend/README.md
Frontend
The frontend is a Next.js app that provides:
- User authentication (login, register)
- Browse/search events and view details
- Register for events and pay via Yoco
- Manage and view tickets
Documentation: see Frontend README for environment and scripts.
- Frontend README: ./frontend/README.md
Getting Started (Local Development)
Prerequisites
- Node.js 18+ (recommended)
- npm or yarn
- PostgreSQL database
1) Install dependencies
-
Backend
- cd backend
- npm install
-
Frontend
- cd ../frontend
- npm install
2) Configure environment variables
-
Backend: create backend/.env and set required variables (see backend/README.md). At minimum:
- DATABASE_URL
- JWT_SECRET
- PORT (optional; defaults to 3000 if not set)
- YOCO_SECRET_KEY (for payments)
- YOCO_WEBHOOK_SECRET (to verify webhooks)
- EMAIL_HOST, EMAIL_PORT, EMAIL_USER, EMAIL_PASS, EMAIL_FROM (for ticket emails)
- FRONTEND_URL or APP_BASE_URL (used in email links)
-
Frontend: create frontend/.env.local and set
- NEXT_PUBLIC_API_URL=http://localhost: Example for default backend: NEXT_PUBLIC_API_URL=http://localhost:5000
-
Organisation name, logo, brand colour, and notification emails are configured via the admin panel (Admin → Site Settings) and do not need to be set in .env files.
3) Run the apps
-
Start backend (development):
- cd backend
- npm run dev
- By default runs at http://localhost:3000 (or the PORT you set)
-
Start frontend (development):
- cd ../frontend
- npm run dev
- Next.js dev server runs at http://localhost:3000 by default; if it conflicts with backend, it will pick another port (e.g., 3001). Ensure NEXT_PUBLIC_API_URL points to the backend port.
Deployment
Backend Deployment
- Provision a PostgreSQL database and set DATABASE_URL.
- Set all required environment variables (see backend/README.md).
- Run Prisma migrations (if applicable).
- Start the server with: npm start (or a process manager like PM2).
- Expose the webhook endpoint /api/webhooks/yoco publicly and configure the YOCO webhook to point to it.
- Ensure the uploads directory public/uploads exists and is writable.
Frontend Deployment
- Build the frontend:
- cd frontend
- npm run build
- Run with npm start (Next.js) on your server, or deploy to a platform like Vercel.
- Ensure NEXT_PUBLIC_API_URL points to the public URL of your backend (including port if not 80/443).
Ports and URLs
- Backend default port is 3000 unless overridden by PORT.
- Frontend dev server commonly uses 3000; set NEXT_PUBLIC_API_URL to avoid clashes (e.g., backend 3000, frontend 3001).
Useful Links
- Backend API Documentation: ./backend/API_DOCUMENTATION.md
- Backend README: ./backend/README.md
- Frontend README: ./frontend/README.md
Recent Changes
Sold-out handling, tiered stock warnings & UI polish
Backend
GET /api/eventsnow computes and returnsisSoldOut: booleanper event — true when every option with a stock limit (stockLimit > 0) is fully sold out. Events with only unlimited options are never sold out.POST /api/settings/test-smtperror responses now include arawfield (the original error message) alongside the human-readablemessage. Error code 530 (Microsoft "Client not authenticated to send mail") is now correctly mapped to the authentication-failure message.
Frontend
- Event listing (
/events):EventCardshows a disabled "Sold Out" button (red tint) whenisSoldOutis true, replacing the Register button. Free events now show "Register" without the "- R0.00" suffix. - Event detail page (
/events/[id]): computes sold-out state from returned optionavailableCount; shows a disabled "Sold Out" button instead of the Register link. Options with price0display "Free" instead of "R0.00". - Registration page (
/register/[eventId]): shows a red "This event is sold out" banner and replaces the Register button with a disabled "Sold Out" button when all limited options are exhausted. - Tiered low-stock threshold: the "X remaining" badge now uses capacity-based percentages — ≤ 50 tickets: 20 %; 51–200: 15 %; 201–1,000: 10 %; 1,000+: 5 %. Changed from
Math.ceiltoMath.roundto prevent rounding-up inflation of the threshold. Fixed a React rendering bug wherestockLimit && stockLimit > 0 && ...evaluated to the number0(which React renders as visible text) instead of a boolean — this caused "00" to appear next to options with unlimited stock (stockLimit = 0). - Event image upload: the EventForm (used in the create modal) now shows a file upload button with inline preview alongside the URL input field. Files are uploaded to
POST /api/uploads/event-imageand the returned URL is set automatically. - Attachments during event creation: step 6 ("Attachments") of the supervisor create-event wizard now lets you stage files. They are uploaded to
POST /api/events/:id/attachmentsimmediately after the event record is created, so attachments no longer require a separate edit session. - SMTP test details: on failure the "Test connection" result now shows a collapsible "Show technical details" section exposing the raw SMTP error alongside the friendly message.
ApiErrorinsrc/lib/api.tsnow carries adatafield with the full JSON response body.
Variants, stock limits, early-bird pricing & cancellation guard
Backend
OptionVariantmodel: event options can now have sub-variants (sizes, colours, ticket types — e.g. Adult/Child/VIP under a single "Ticket" option). Variants have an optionalpriceoverride and their ownstockLimit.- Stock limits:
stockLimitadded toEventOptionandEarlyBirdTier. A value of0means unlimited. All stock counts exclude cancelled registrations. - Early-bird concurrency handling: once a Yoco checkout has been issued at an early-bird price, that price is honoured even if another user simultaneously exhausts the tier's stock. Stock-based forfeiture only applies at registration time (via
resolveOptionPrice). Deadline expiry at payment time forfeits the price; stock races after the checkout is issued do not. - Early-bird stock forfeiture: if a tier's deadline passes after registration but before payment,
refreshPricingForRegistration(called before every Yoco checkout) re-evaluates the tier and updatespriceSnapshot. If prices changed,POST /api/payments/yoco-checkoutreturns{ priceUpdated: true, newTotal }(HTTP 200) so the frontend can show a warning before retrying. priceSnapshot/appliedTierIdonRegistrationOption: price and tier are snapshotted at registration creation.computeRegistrationTotalDueusespriceSnapshotas authoritative when present (legacy rows without a snapshot fall back to deadline-only re-evaluation).- Cancelled registration tickets:
GET /api/tickets/myticketsnow excludes tickets from cancelled registrations at the database level. - Cancellation payment guard:
DELETE /api/registrations/:idblocks non-admins from cancelling a registration that has positive payments. Admins can cancel at any time. - Variant CRUD:
POST /api/events/options/:id/variants,PUT /api/events/variants/:id,DELETE /api/events/variants/:id. - Testing mode (
NODE_ENV=testing): behaves like development (permissive CORS, all debug routes) but validates required env vars (DATABASE_URL,JWT_SECRET) at startup and enables rate limiting.
Frontend
- Register page (
/register/[eventId]): variant picker — when an option has variants, shows each variant with its own qty control, stock badge (sold out / X remaining at ≤ 15 %), and price. Submission payload includesvariantId. Early-bird notice shown when any active tier is present. - Event detail page (
/events/[id]): ticket section shows variant breakdown with "From R…" pricing, early-bird strikethrough, and sold-out/nearly-out badges. - User dashboard: "Cancel registration" button appears in the registration modal when no payments have been made. Inline confirmation before the API call.
- Pay page: if the backend returns
priceUpdated: true, an amber warning banner shows the new total and a dismiss button before the user can retry. - Supervisor events page (
/dashboard/supervisor/events): 6-step create/edit modal — "Basic Details", "Items & Pricing", "Sections", "Form", "Visibility", "Attachments". In create mode, step tabs are not clickable (linear navigation only); in edit mode any step can be jumped to. Options in "Items & Pricing" are collapsible cards; Variants and Early-bird tiers are expandable sub-sections within each option./dashboard/supervisor/sectionsnow redirects to the Events page. - Legal pages: Terms of Use updated with early-bird pricing terms, partial payment terms, and self-cancellation policy. Privacy Policy updated with data anonymisation details.
Site settings & first-time setup wizard
- Admin → Site Settings (
/dashboard/admin/settings): organisation details, branding (colour + logo), notification emails, SMTP email delivery, and legal page content are stored in the database and editable at runtime. - Setup wizard (
/setup): on a fresh deployment (empty database) the platform automatically shows a wizard — organisation details, admin account creation, and branding. No manual DB seeding required. /api/settings(public): exposes public settings (org details, legal keys) for navbar and legal pages./api/setup(public, one-time): creates the first admin account; blocked once any user exists.- Settings cache (
settingsCache.js): in-process 60-second cache with transparent AES-256-GCM decryption for sensitive keys. - SMTP via admin panel: host, port, TLS, from address, username, and password are all configurable without touching
.env. Password is stored AES-256-GCM encrypted (key derived fromJWT_SECRET). - Legal pages dynamic: Terms of Use and Privacy Policy populate org name, contact email, website URL, operator name, Information Officer details, and effective date from the settings DB.
ORG_NAME,ORG_TAGLINE,EMAIL_HEADER_COLOR,REGISTRATIONS_EMAIL, SMTP vars no longer required in.env— all managed via the admin panel (env vars still work as fallbacks).
Admin dashboard improvements
- User management (
/dashboard/admin/users): filtering (role, active status, text search) is now server-side so page size is always consistent; per-page selector (10/25/50/100); "Delete data" button anonymises a user's personal information viaPOST /api/users/:id/anonymize. - Registrations (
/dashboard/admin/registrations): event dropdown, status, and fuzzy text filters; expandable rows showing ticket options and form responses loaded on demand.
Forms page (/dashboard/supervisor/forms, /dashboard/admin/forms)
- Print opens a clean popup window (no site chrome) with one form response per A4 page — event name, attendee details, registration ID, and answers in a two-column grid.
- User filter replaced raw "User ID" text input with a searchable name/email/phone dropdown.
- Event filter includes "Include past events" and "Include inactive events" checkboxes.
- Filters in a responsive grid; dropdowns constrained to
max-w-xs.
Ticket printing
- At-the-door individual print: individual ticket print now generates a proper A6 ticket with QR code, event title, date, type, holder name, and ID — no longer just a UUID.
- Staff event-tickets page: A4 print layout matches at-the-door (2 columns × 4 rows, 8 tickets per page).
Form builder & events
- Form builder: up/down reorder buttons; type badge (Text/Number/Date/etc.); separate quick-add buttons for Statement and Heading fields.
- Early-bird tiers: no manual order number — tiers ordered by list position; labelled Deadline and Price inputs.
Bulk messaging dropdowns
- Attendee selection dropdown constrained to
max-w-xs/w-64panel on email-attendees and whatsapp-attendees pages.
Backend
GET /api/userssupports server-side?search=,?role=,?isActive=filtering.POST /api/users/:id/anonymize— erases personal data (admin only).GET|PUT /api/settings,GET /api/settings/needs-setup,POST /api/setup— settings and setup endpoints.POST /api/uploads/logo— logo upload (admin, or unauthenticated during first-time setup).backend/src/utils/encryption.js— AES-256-GCM encrypt/decrypt; key derived fromJWT_SECRETvia scrypt.- SMTP transporter rebuilt dynamically when settings cache detects a config change — no restart required after updating SMTP settings.
License
This project is licensed under the MIT License.