Events, payments, registrations, scan locks and tickets marked; list
controllers filtered (registrations, payments, tickets, scans, stats,
coupons, scheduled messages, form responses, funnel). Owner-or-staff routes
check staff against the record's event (ownerOk). Scanner names the campus
of another campus's ticket. Route test passes; unit tests for the rules.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- PayFast (custom integration): signed form (PHP-style encoding, checked
against a real sandbox), ITN checks (signature over all posted fields,
PayFast host, VALID from /eng/query/validate, amount vs checkout).
- ProviderCheckout records every checkout (amount, metadata, request);
webhooks match payments to it and hold a different amount for review
(ProviderTransaction.note).
- /pay/<id>: payers choose a provider when several are on, and the
browser posts PayFast's form from there, so payment links work for
every provider.
- Site settings → Payments: PayFast card (merchant ID/key, encrypted
passphrase, sandbox/live) while PayFast is enabled.
- Payer-facing wording no longer names Yoco; legal pages name both.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- src/payments: provider registry and PaymentProvider interface
(createCheckout, verifyWebhook/parseWebhook, getTransaction, refund,
listTransactions as capabilities), Yoco as the first provider. All four
direct Yoco checkout calls go through payments.createCheckout.
- Webhook split into the provider part (signature, parsing) and shared
processing; same URL (/api/webhooks/yoco), same behaviour.
- YocoTransaction renamed in place to ProviderTransaction (+ provider,
unique per provider + externalId); Payment.provider added and
backfilled to "yoco". Payment.method stays the wallet type so cashups
are unchanged.
- Site settings → Payments: Enabled toggle and order per provider; Yoco's
settings shown while it's enabled. R15 minimum for all online payments.
- New endpoint names (/api/payments/checkout, /api/provider-transactions);
the old /yoco-checkout and /yoco-transactions paths still work.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The register form now offers a "Donation amount" (suggested amounts,
custom, or none) under the ticket options. The donation is added to the
Yoco checkout as its own line and the webhook records it as a separate
donation payment linked to the registration payment.
Move soldOutLines() out of the "use client" JoinWaitlistButton module
into lib/waitlist.ts so the server-rendered event page can call it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Sell a set of tickets together for one price (e.g. 5 Adult + 3 Child for R500).
Bundles apply automatically, as many times as a registration's tickets allow, and
only when cheaper than the prices actually charged. Coupons come off afterwards.
- EventBundle/EventBundleItem/RegistrationBundle models and Registration.bundleDiscount
- Supervisor "Bundle deals" page, quote endpoint, bundle lines on invoices,
receipts, emails, WhatsApp and Yoco checkout, Discounts report rows
- Register form shows started bundles with "Complete bundle" and "+" buttons
- Demo: Family Fun Day sells a Family deal bundle instead of the Family pass ticket
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The Yoco secret key and webhook secret now live in AppSetting (AES-256-GCM
encrypted), with .env as a fallback. A new Site Settings → Yoco tab shows
them masked card-style, registers a webhook with Yoco (storing the secret it
returns) or accepts one entered by hand, and lists/deletes the account's
webhooks. Generic settings endpoints never read or write these keys.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Percentage, fixed-amount and comp codes, limited by event, ticket option
or variant, dates, use counts, minimum quantity, and people, roles or an
email domain. The discount is locked in on the registration (mirrored on
Registration.discountTotal so every total sees it), shows on invoices,
receipts, emails and the Yoco checkout, and is released on cancellation.
Use limits are enforced under an advisory lock.
Ticket options and variants can also carry a staff price that Staff and
above get automatically. Adds a Supervisor > Coupon codes page, code
fields on registration, My Events, manual registration, at the door and
the kiosk, a Discounts report, and audit log entries. Also fixes Yoco
line items and the free-registration check for lines spanning several
price tranches.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Six site improvements picked from a "what could be better" review, plus a Jest
test suite covering the two areas with the trickiest money-handling history
in this project (early-bird pricing tranches, donation-leg accounting):
- "Add to calendar" .ics download on event pages and in confirmation emails
- sitemap.xml, robots.txt, and Open Graph/Twitter metadata for public pages
- Sentry error monitoring (backend + frontend), a no-op until SENTRY_DSN is set
- Nightly local pg_dump backups with a Site Settings tab to browse/trigger/download
- Admin audit trail for refunds, donations, manual registrations, event and
settings changes, and staff-initiated cancellations
- Jest tests reproducing and guarding against the 1.8.0 tranche-pricing bug
and the 1.4.2 donation-balance-inflation bug
Wallet passes (Google/Apple) were scoped out of this round — Apple Wallet
needs a paid Apple Developer account the project doesn't have yet, and the
user preferred shipping both together later rather than Google alone now.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
- Registration confirmations attach an invoice PDF (itemized breakdown,
early-bird discount, balance due, Yoco pay-now link/QR) whenever a
balance is outstanding; payment/donation confirmations attach a
payment receipt PDF. Sent as an email attachment and, over WhatsApp,
as the PDF itself with the existing message as its caption.
- Users can also (re)send either document on demand: an "Invoice"
button on the registration detail popup, and a "Receipt" button next
to each payment there and on the Payment history page, each opening
an Email/WhatsApp choice popup, via two new endpoints restricted to
the registration/payment's own owner.
- Fix: editing an event option's early-bird tiers deleted and
recreated every tier for that option with brand-new ids, silently
severing the appliedTierId link on all historical purchases (losing
early-bird attribution and undercounting stock-limit usage) even for
tiers the admin didn't touch. Tiers are now upserted by id.
- Update the "My Events" help content and the API docs index for the
new endpoints.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
- Early-bird pricing: RegistrationOption now tracks each purchase as a
separate price tranche instead of overwriting a single price/quantity
on repeat purchases, so buying more tickets after a tier expires no
longer re-prices tickets already bought at the old price. Stock-limit
checks, total-due calculation, and the Finance report's revenue-by-
option are all tranche-aware; pages that showed one blended price per
line now render/total each tranche. Viewing a pending/partially-paid
registration (dashboard, detail page, or an event's registration
list) now refreshes stale pricing on the spot instead of only at
payment time.
- Fixed the "(early bird)" dashboard label incorrectly firing on any
line priced below the base option price (e.g. a plain cheaper
variant) — it now checks the real applied-tier flag.
- Added contact-only events (e.g. baptism): no registration/payment
flow, shown on the public site with a "Contact us" popup instead of
a Register button. Configurable via the admin event wizard.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
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>
Reconciling a Yoco payment and sending payment links blocked the HTTP
response on ticket-PDF generation and email/WhatsApp sends; they now
run in the background like the other payment flows already did.
Registration/payment option loops (pricing, stock checks, ticket
generation) now resolve concurrently instead of sequentially. The
Payments page dropped a per-registration N+1 fetch and now refreshes
its lists in parallel after each action. Added missing indexes for
dashboard stats and donation-leg lookups, and made GET
/api/registrations optionally paginated.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
- Supervisor Payments: mode tabs now wrap on mobile instead of overflowing off-screen.
- Reconciling a Yoco transaction as a donation now lets staff pick who it's from.
- Reports payment-method breakdown now buckets into Cash/Card/EFT/Other everywhere,
folding Apple Pay, Google Pay, and Yoco-portal payments into Card.
- Refunds now net against their original method's bucket (Cashup/Finance/Profit reports,
My Payments filtering/display) instead of vanishing or falling into "Other".
- Refund form's method dropdown mirrors the real payment methods and auto-fills from the
payment being refunded, replacing an ambiguous generic "Refund" option.
- Fixed donation remaining/unallocated balance inflating instead of shrinking when a
donation is refunded (assign-donation endpoint, cashup reports, donations reports,
and the Assign Donation panel all summed refund legs with the wrong sign).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Financial correctness (donation-leg model):
- Donations are no longer mutated when assigned to a registration; assignment now
creates an immutable "leg" record referencing the original donation instead.
- Fixed several places where money was double-counted once a donation was partially
or fully assigned (Payments, Revenue summary, Cashup reconciliation, Finance
report, Profit report, Master Orders, Revenue Detailed).
- Payments now record who recorded them (recordedBy), separate from who they're for.
Cashup:
- Per-user cash denomination counting (optional, any time) replaces the single
event-wide manual entry; the event's cash actual is the live sum of these counts.
- New "Payment accountability by staff member" breakdown across all methods, and a
read-only "Report" tab that opens automatically once an event is closed.
Reports page redesign:
- New shell: sidebar of universal filters (events, date range, past/inactive/closed
toggles), searchable/categorized report grid, and a popup viewer with
Print/Email/Excel/WhatsApp actions plus an in-app Reporting Guide.
- Visual pass: colored stat tiles and bar charts on most reports, matching mockups.
- PDF exports (download/Print/Email/WhatsApp) now share a branded design mirroring
the web report — colored header, stat tiles, bar chart, highlighted totals.
- Excel export now produces a styled .xlsx (via exceljs) instead of a plain CSV.
- Master Orders' "Donations made" table is now included in every export channel.
Bug fixes discovered while testing exports:
- Report emails now go through the shared, DB-configurable mail utility instead of
a one-off transporter that ignored Site Settings SMTP config.
- WhatsApp report sends now surface the actual WAWP API error and auto-recover a
disconnected session, instead of a bare axios status-code message.
Also: Admin-editable notification preference, richer Admin Registrations dashboard,
{{payment.link}} placeholder for Email/WhatsApp Attendees, and background
email/WhatsApp attendee sending.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Per feedback: card-network wallet payments (Apple Pay, Google Pay) should
report and filter as "card" on the user payment history page rather than
"other", since they settle the same way as a card payment. Any other
gateway-reported method still falls under "other".
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Per feedback, drop the dynamic /mypayments/methods lookup (wasn't
loading reliably) in favor of a static cash/card/eft/voucher/other
dropdown. Server-side normalization now folds any gateway-reported
method outside those four manual-entry values (apple_pay, google_pay,
yoco, etc.) into "other" instead of "card", both in the returned data
and in the filter query.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Per feedback, stop trying to surface every gateway-reported wallet
type (apple_pay, google_pay, yoco, ...) as its own filter/display
value on the user payment history page. Both /mypayments and
/mypayments/methods now fold anything outside the four manual-entry
methods into "card", both in the returned data and in the filter
query, so Apple Pay/Google Pay payments show up under Card.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
The previous fix hardcoded apple_pay/google_pay as extra filter options,
but Payment.method is free-text set by whatever the gateway reports, so
guessing at literal values was fragile and still didn't surface them for
this user. Add GET /api/payments/mypayments/methods returning the
distinct method values actually present in the user's payments, and have
the dashboard filter build its options from that instead. Also switch
the method filter from a startsWith match to an exact match, since the
values now come straight from the same column being filtered.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
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.
Extends the earlier user-dashboard fix: canEditActive is renamed
canModifyActive and now also gates the "Make payment" and "Cancel
registration" actions, not just editing. Backend enforcement added to
cancelRegistration and createYocoCheckout to block past-event
self-service payment/cancellation server-side (cashup-closed events
were already blocked via assertEventOpen; admins/supervisors are
exempt from the past-date check, consistent with existing overrides).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>