Initial commit
Next.js + Express event management app for Hope Family Church.
This commit is contained in:
@@ -0,0 +1,528 @@
|
||||
# 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](#overview)
|
||||
2. [Tech Stack](#tech-stack)
|
||||
3. [Prerequisites](#prerequisites)
|
||||
4. [Installation](#installation)
|
||||
5. [Environment Variables](#environment-variables)
|
||||
6. [Database](#database)
|
||||
7. [Running the Server](#running-the-server)
|
||||
8. [Authentication & Roles](#authentication--roles)
|
||||
9. [API Reference](#api-reference)
|
||||
10. [Background Workers](#background-workers)
|
||||
11. [Notification System](#notification-system)
|
||||
12. [WhatsApp Integration](#whatsapp-integration)
|
||||
13. [File Uploads](#file-uploads)
|
||||
14. [Payments (Yoco)](#payments-yoco)
|
||||
15. [Deployment](#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
|
||||
|
||||
```bash
|
||||
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
|
||||
|
||||
```bash
|
||||
# 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
|
||||
|
||||
```bash
|
||||
# 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
|
||||
|
||||
### PM2 (recommended)
|
||||
```bash
|
||||
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
|
||||
Reference in New Issue
Block a user