Files
joshua 37681aec50 Add paginated payment history to user dashboard
Users can now view their own payments (excluding donations) with
server-side pagination (25/page), date range, method, and
payment/refund filters, both on the API and the new client page.
2026-07-23 18:02:53 +02:00
..
2026-07-23 15:26:47 +02:00
2026-07-23 15:26:47 +02:00
2026-07-23 15:26:47 +02:00
2026-07-23 15:26:47 +02:00
2026-07-23 15:26:47 +02:00
2026-07-23 15:26:47 +02:00
2026-07-23 17:08:28 +02:00
2026-07-23 15:26:47 +02:00

Hope Events — Backend API

Node.js + Express REST API for the Hope Events platform used by Hope Family Church (events.hopehenley.co.za).


Table of Contents

  1. Overview
  2. Tech Stack
  3. Prerequisites
  4. Installation
  5. Environment Variables
  6. Database
  7. Running the Server
  8. Authentication & Roles
  9. API Reference
  10. Background Workers
  11. Notification System
  12. WhatsApp Integration
  13. File Uploads
  14. Payments (Yoco)
  15. Deployment

Overview

Full-stack event management backend handling:

  • User registration, authentication (JWT), and role-based access control
  • Event creation, management, and public listings
  • Attendee registration with optional payment tiers and early-bird pricing
  • Yoco checkout integration with webhook reconciliation
  • Ticket generation (PDF + QR code) and email/WhatsApp delivery
  • Scheduled bulk messaging (email + WhatsApp) for attendees and broadcast lists
  • Admin WhatsApp instance management via WAWP

Tech Stack

Layer Technology
Runtime Node.js 18+
Framework Express 4
Database PostgreSQL (via Prisma ORM 5)
Auth JWT (jsonwebtoken), bcryptjs
Email Nodemailer (any SMTP — configured via Admin → Site Settings, AES-256 encrypted in DB)
WhatsApp WAWP REST API (api.wawp.net/v2)
PDF/QR PDFKit, QRCode
File Uploads Multer
Payments Yoco

Prerequisites

  • Node.js 18+
  • PostgreSQL 14+ (hosted at database.crosscode.co.za for production)
  • npm

Installation

cd backend
npm install           # installs deps and runs `prisma generate`

Environment Variables

Create backend/.env from .env.example. The only variables you must set are:

Variable Required Description
DATABASE_URL Yes PostgreSQL connection string
JWT_SECRET Yes Secret key for JWTs (32+ random characters). Also used to derive the AES-256 encryption key for sensitive settings (SMTP credentials) stored in the DB — must be set before saving SMTP settings.
FRONTEND_URL Yes Comma-separated allowed CORS origins
YOCO_SECRET_KEY Yes Yoco secret API key
YOCO_WEBHOOK_SECRET Yes Yoco webhook HMAC secret
APP_BASE_URL Public frontend URL — used in email links (fallback if not set via admin panel)
BACKEND_URL Public backend URL — used to serve ticket PDFs over WhatsApp
PORT Port to listen on (default 3000)
NODE_ENV production or development
WAWP_ACCESS_TOKEN WAWP fallback token (preferred: set via Admin → Site Settings)
WAWP_INSTANCE_ID WAWP fallback instance ID (preferred: set via Admin → Site Settings)
DAILY_SUMMARY_ENABLED false to disable daily summaries (default true)
SCHEDULED_EMAILS_ENABLED false to disable scheduled-send worker (default true)
SCHEDULED_EMAILS_INTERVAL_MS Polling interval in ms (default 30000)
SMTP_HOST / EMAIL_HOST Fallback SMTP host (preferred: set via Admin → Site Settings)
SMTP_PORT / EMAIL_PORT Fallback SMTP port
SMTP_USER / EMAIL_USER Fallback SMTP username
SMTP_PASS / EMAIL_PASS Fallback SMTP password
MAIL_FROM / EMAIL_FROM Fallback from address

Organisation name, branding colour, logo, registration notification email, SMTP settings, and legal page content are now managed through the admin panel at Admin → Site Settings and stored in the database. You no longer need ORG_NAME, ORG_TAGLINE, EMAIL_HEADER_COLOR, REGISTRATIONS_EMAIL, or SMTP vars in .env (they still work as fallbacks). On a fresh deployment these are set via the first-time setup wizard.

SMTP credentials (smtp_user, smtp_pass) are stored AES-256-GCM encrypted in the database. The encryption key is derived from JWT_SECRET. Do not change JWT_SECRET after saving SMTP credentials — the stored values will become unreadable.


Database

Schema summary

Model Purpose
User Accounts with roles (admin, supervisor, staff, user)
Event Events with dates, pricing, options, attachments
EventOption Ticket types / items per event; stockLimit (0 = unlimited), isMainTicket flag
EarlyBirdTier Time-limited discounted prices; linked to an EventOption (option-level) or an OptionVariant (variant-level via nullable variantId); stockLimit (0 = unlimited)
OptionVariant Sub-items per option (size, colour, ticket type); optional price override; own stockLimit
Registration A user's registration for an event; status: pending/confirmed/partial_paid/paid/cancelled
RegistrationOption Which options (and quantities) a registration includes; priceSnapshot, appliedTierId, variantId
Payment Payment records (Yoco, manual, donation, refund)
Ticket QR-code tickets linked to registration options
TicketUsage Scan log for each ticket redemption
PasswordReset One-time tokens for password reset flow
EventAttachment Files attached to events (PDFs, images)
EventForm / EventFormField Per-event custom forms
FormResponse / FormAnswer Submitted form data from attendees
FormDraft Auto-saved in-progress form data
YocoTransaction Raw webhook events from Yoco for reconciliation
Section / SectionOption Grouping of event options into sections
AppSetting Key-value store for runtime config — WAWP credentials, org name, branding, notification emails, SMTP settings (credentials stored AES-256-GCM encrypted), legal page content

Migrations

# Apply pending migrations (production)
npm run prisma:deploy

# Check migration status
npm run prisma:status

# Regenerate Prisma client after schema changes
npm run prisma:generate

Running the Server

# Development (auto-reload)
npm run dev

# Production
npm start

Visit / for the status page. Visit /docs?token=<admin-jwt> for full API documentation (admin only).


Authentication & Roles

All protected routes require a Bearer token in the Authorization header:

Authorization: Bearer <jwt>

Tokens are issued on POST /api/users/login. Token version is stored per-user; revoking sessions increments the version and invalidates all existing tokens for that user.

Role Hierarchy

Role Description
admin Full access — user management, delete operations, WhatsApp admin, payment stats
supervisor Event/registration/payment management, bulk messaging, reports
staff Read access to registrations, payments, tickets; ticket scanning
user Own profile, own registrations, own tickets

Middleware

Middleware Applied when
protect Any authenticated route
admin Admin-only actions
supervisor Supervisor + admin
staff Staff + supervisor + admin
optionalAuth Routes that work for guests but enrich responses if authenticated
loginLimiter Rate-limits login to 15 req/min per IP

API Reference

Full interactive API docs available at GET /docs (requires admin JWT — pass via ?token=<jwt> query param or Authorization: Bearer header).

Quick reference by domain

Users — /api/users

Method Path Auth Description
POST / public Register a new account
POST /login public Login, returns JWT
POST /forgot public Request password reset email
POST /reset public Reset password with token
POST /activate public Activate account from email link
GET /profile user+ Get own profile
PUT /profile user+ Update own profile
POST /revoke-sessions user+ Revoke own sessions
POST /close-account user+ Deactivate own account
GET / supervisor+ List all users (paginated); supports ?search=, ?role=, ?isActive=true|false, ?page=, ?limit=
GET /:id admin Get user by ID
PUT /:id admin Update user
DELETE /:id admin Deactivate user (sets isActive: false)
POST /:id/revoke-sessions admin Revoke all sessions for a user
POST /:id/anonymize admin Erase personal data — sets name to "Deleted User", clears email/phone, deactivates account, revokes all sessions

Events — /api/events

Method Path Auth Description
GET / public List public/active upcoming events; response includes isSoldOut: boolean per event
GET /all staff+ All events including hidden/past
GET /admin/all admin All events (admin view)
GET /attachments/status admin Attachment manifest sync status
POST /attachments/sync admin Re-sync attachment manifests to DB
GET /:id public Get event by ID
GET /by-alias/:redirectUrl public Get event by redirect alias
POST / supervisor+ Create event
PUT /:id supervisor+ Update event
DELETE /:id admin Delete event
GET /:id/attachments public List event file attachments
POST /:id/attachments supervisor+ Upload attachment
DELETE /:eventId/attachments/:id supervisor+ Delete attachment
POST /:id/email-attendees supervisor+ Bulk email attendees (supports dryRun)
POST /:id/email-attendees/schedule supervisor+ Schedule bulk email
POST /:id/whatsapp-attendees supervisor+ Bulk WhatsApp to attendees (supports dryRun)
POST /:id/whatsapp-attendees/schedule supervisor+ Schedule bulk WhatsApp
POST /:id/options supervisor+ Add event option
PUT /options/:id supervisor+ Update event option
DELETE /options/:id admin Delete event option

Registrations — /api/registrations

Method Path Auth Description
POST / optional Create registration
GET /myregistrations user+ Own registrations
PUT /:id/options user+ Update registration options
DELETE /:id user+ Cancel registration
GET /:id optional Get registration by ID
GET /:id/forms/draft optional Get form draft
PUT /:id/forms/draft optional Save form draft
POST /:id/forms/responses optional Submit form responses
PUT /:id/forms/responses supervisor+ Replace form responses
GET / staff+ List all registrations
PUT /:id staff+ Update registration status
GET /event/:eventId staff+ Registrations for an event
POST /manual supervisor+ Manual registration

Payments — /api/payments

Method Path Auth Description
POST / supervisor+ Record manual payment
POST /yoco-checkout user+ Initiate Yoco checkout
GET /mypayments user+ Own payments
GET /:id user+ Payment by ID
GET /registration/:registrationId user+ Payments for a registration
GET / supervisor+ All payments
GET /event/:eventId staff+ Payments for an event
PUT /assign-donation supervisor+ Assign donation to registration
POST /refund supervisor+ Create refund
GET /admin/stats admin Payment statistics

Tickets — /api/tickets

Method Path Auth Description
GET /mytickets user+ Own tickets
GET /:id user+ Ticket by ID
POST /email user+ Email own tickets
POST /send-to staff+ Send tickets to a specific user
GET /scan-preview/:qrCode staff+ Preview scan (no mark)
GET /qr/:qrCode staff+ Ticket by QR code
POST /scan/:qrCode staff+ Scan/redeem ticket
GET /event/:eventId staff+ All tickets for event
GET /scans/recent staff+ Recent scan activity
GET /scans/stats staff+ Scan statistics
POST /generate supervisor+ Generate tickets for registration
GET / supervisor+ All tickets
PUT /email-sent admin Bulk mark tickets as emailed

Webhooks — /api/webhooks

Method Path Auth Description
POST /yoco HMAC Yoco payment webhook
POST /whatsapp WAWP inbound webhook

Broadcasts — /api/broadcasts (email)

Method Path Auth Description
POST /preview supervisor+ Dry-run, returns recipient count/sample
POST /send supervisor+ Send email broadcast
POST /schedule supervisor+ Schedule email broadcast

WhatsApp Broadcasts — /api/whatsapp-broadcasts

Method Path Auth Description
POST /preview supervisor+ Dry-run
POST /send supervisor+ Send WA broadcast
POST /schedule supervisor+ Schedule WA broadcast

Scheduled Messages — /api/scheduled-emails

Method Path Auth Description
GET / supervisor+ List all scheduled jobs
PATCH /:id supervisor+ Edit a queued job
DELETE /:id supervisor+ Cancel a queued job

Automations — /api/automations

Method Path Auth Description
POST /schedule supervisor+ Schedule event lifecycle automations

Reports — /api/reports

Method Path Auth Description
POST /pdf user+ Generate and download PDF report
POST /email user+ Email PDF report to current user

Settings — /api/settings

Method Path Auth Description
GET / public Public settings (org name, colour, logo, legal keys, etc.)
GET /all admin All settings — smtp_pass returned masked (••••••••), smtp_user decrypted
GET /needs-setup public { needsSetup: bool } — true until setup wizard completes
PUT / admin Upsert settings; smtp_user and smtp_pass are AES-256-GCM encrypted before storage; sending •••••••• for smtp_pass is a no-op (keeps existing value)
POST /test-smtp admin Test SMTP connection with provided credentials; success returns { message }, failure returns { message, raw } where message is human-readable and raw is the original SMTP error. The short-lived admin JWT returned by POST /api/setup/register also qualifies during the setup wizard.

Setup — /api/setup

Method Path Auth Description
POST /register public (one-time) Step 2 of setup wizard — creates the first admin account and returns a short-lived JWT setup token; blocked once any user exists
POST / setup token or admin Final step of setup wizard — saves initial site settings (SMTP, org name, branding); requires setup token from /register

Uploads — /api/uploads

Method Path Auth Description
POST /event-image supervisor+ Upload event cover image (image field)
POST /logo admin (or no-auth during setup) Upload site logo (image field)

Sections — /api/sections

Method Path Auth Description
GET / staff+ List sections
POST / supervisor+ Create section
PUT /:id supervisor+ Update section
DELETE /:id admin Delete section

Banner — /api/banner

Method Path Auth Description
GET / public Get current site banner
POST / supervisor+ Set site banner

Forms — /api/forms

Method Path Auth Description
GET /responses staff+ List all form responses

Yoco Transactions — /api/yoco-transactions

Method Path Auth Description
GET / supervisor+ All Yoco webhook transactions
GET /unreconciled supervisor+ Unmatched transactions
POST /:id/reconcile supervisor+ Manually reconcile transaction
POST /:id/ignore supervisor+ Mark transaction as ignored

WhatsApp Admin — /api/whatsapp

Method Path Auth Description
POST /webhook public WAWP inbound event
GET /config admin Get WAWP credentials
POST /config admin Save WAWP credentials to DB
POST /create-instance admin Create WAWP instance
POST /delete-instance admin Delete WAWP instance
GET /status admin WAWP session status
GET /qr admin QR code for WhatsApp linking
POST /request-code admin Request pairing code
POST /logout admin Logout WAWP session
POST /start admin Start WAWP instance
POST /restart admin Restart WAWP instance

Static Files

Path Description
/uploads/* Served uploaded files (images, PDFs)

Background Workers

All workers start automatically with npm start.

Scheduled Emails Worker

Polls every 30 s (configurable via SCHEDULED_EMAILS_INTERVAL_MS) for due jobs. Handles attendee emails, attendee WhatsApp, email broadcasts, and WhatsApp broadcasts. Disable with SCHEDULED_EMAILS_ENABLED=false.

Daily Event Summaries

Runs at 07:00 local time. Sends summary emails to supervisors about that day's events. Disable with DAILY_SUMMARY_ENABLED=false.

Temp File Cleanup

Runs at 03:00 local time. Removes files older than 7 days from public/uploads/tickets-temp/.

Attachment Manifest Sync

Runs once on startup. Imports on-disk attachment manifests not yet in the database.


Notification System

Outbound notifications are routed through src/utils/notify.js:

  • shouldEmail(user) — true if preference is email or both
  • canWhatsApp(user) — true if preference is whatsapp or both and user has a phone number
  • waText(user, message) — sends a WhatsApp text
  • waPdf(user, pdfPath, caption) — copies PDF to temp, sends URL to WAWP, cleans up after 5 min

Automated notifications: login, account deletion, ticket delivery, payment confirmation, password reset, daily summaries.

Dynamic placeholders in bulk messages: {{name}}, {{event.title}}, {{event.start}}, {{event.link}}, {{balance}}, {{promo.title}}, {{promo.link}}.


WhatsApp Integration

WAWP API (api.wawp.net/v2). All calls go through src/utils/whatsapp.js.

Credentials are stored in AppSetting (WAWP_ACCESS_TOKEN, WAWP_INSTANCE_ID), with .env as fallback. Cached in memory for 30 s.

"Session not found" recovery: stale session is deleted and a new one is created automatically, saving the new instance_id to AppSetting.

Admin UI: /dashboard/admin/whatsapp.


File Uploads

  • Event images: POST /api/uploads/event-image (field image), saved to public/uploads/
  • Event attachments: POST /api/events/:id/attachments (field file)
  • Ticket PDFs: generated on demand, temporarily in public/uploads/tickets-temp/, auto-deleted after 5 min

Files are served from /uploads/*.


Pricing & Cancellation Rules

Cancellation

DELETE /api/registrations/:id

Actor Condition Result
Owner (user) No positive payments recorded 200 — status set to cancelled
Owner (user) Any positive payment exists 403 — must contact organisation
Admin Any state 200 — always allowed
Anyone Already cancelled 400

Cancelled registrations are excluded from all stock-limit counts (option, variant, and tier stock).

Early-bird pricing — two-phase evaluation

Prices are evaluated in two phases:

Phase 1 — Registration time (resolveOptionPrice in src/utils/pricing.js)

  • Selects the cheapest applicable tier, checking both the deadline and the stock limit.
  • Result stored as priceSnapshot and appliedTierId on each RegistrationOption.
  • Tier sort order: cheapest first; earliest deadline breaks ties.

Phase 2 — Payment time (refreshPricingForRegistration in src/utils/pricing.js)

  • Called automatically before any Yoco checkout is created or any manual payment is recorded.
  • Re-runs resolveOptionPrice for every RegistrationOption that has an appliedTierId.
  • If a tier has since expired or its stock has been exhausted, priceSnapshot and appliedTierId are updated in the DB to reflect the next applicable tier or the base price.
  • If prices changed, POST /api/payments/yoco-checkout returns HTTP 200 with { priceUpdated: true, newTotal, message } instead of proceeding to Yoco. The frontend must present the updated total before the user can retry.
  • For manual payments (POST /api/payments) prices are refreshed silently — no blocking response.

computeRegistrationTotalDue (also in src/utils/pricing.js)

  • Uses priceSnapshot as the authoritative price when it is set on a RegistrationOption.
  • Falls back to getEffectiveUnitPrice (deadline-only re-evaluation) for legacy rows without a snapshot.

Variant + tier interaction

Early-bird tiers can be defined at either the option level (no variantId) or the variant level (variantId set). Both are stored as EarlyBirdTier records on the EventOption.

Variant state Effective price
No variant selected Option-level tiers are evaluated via resolveOptionPrice
Variant selected, variant has its own tier(s) Variant-level tiers are evaluated first via resolveVariantTierPrice
Variant selected, no variant-level tier Variant's own price is used (or option price if variant.price is null)

The resolution priority at registration/payment time is: variant tier → variant price → option tier → option base price.


Payments (Yoco)

  1. Frontend calls POST /api/payments/yoco-checkout
  2. Backend calls refreshPricingForRegistration — if any early-bird price changed, returns { priceUpdated: true, newTotal } (HTTP 200) and does not create a checkout. Frontend shows warning; user retries.
  3. On the retry (or first attempt when prices are current), backend creates a Yoco checkout and returns { redirectUrl }.
  4. User pays on Yoco's hosted page.
  5. Yoco sends webhook to POST /api/webhooks/yoco.
  6. Backend verifies HMAC, creates Payment + YocoTransaction, updates registration status, generates tickets.
  7. Unmatched transactions visible at /api/yoco-transactions/unreconciled.

Deployment

npm install -g pm2
pm2 start src/index.js --name hope-events-api
pm2 save && pm2 startup

Checklist

  • DATABASE_URL points to production DB
  • JWT_SECRET is long (32+ chars) and secret
  • FRONTEND_URL lists only production origins
  • NODE_ENV=production
  • Yoco webhook registered and HTTPS
  • WAWP credentials saved via admin panel
  • SMTP credentials correct