- src/payments/status.js: open checkouts, checkCheckouts and the
payment-status job (every 15 min, checkouts 10 min - 3 days old). Found
payments are settled exactly as their webhook would be (settlePayment,
extracted from the webhook handler: amount check, record, emails).
- PayFast: signed API client (alphabetical signature, testing=true in
sandbox), findPayments from the transaction history CSV matched on
M Payment ID, and testConnection (signed /ping). Verified against a
real sandbox account.
- Yoco documents no checkout lookup, so its checkouts are "can't check".
- Payments -> Reconcile: "Waiting for payment" with Check status /
Check all; PayFast card: Test connection.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A shared useOnlinePayments hook (GET /api/payments/providers) gates the
registration success page, My Events (Make payment, Make donation), the
donate page, Sponsor a spot, the register form's donation box and the
supervisor Payment Link tab; they explain why where it helps.
Roadmap: SnapScan and Zapper dropped; later versions move up by two.
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>
- Waitlist per session: entries for pick-sessions tickets name a session;
offers use that session's free places, never run past its start, and
claim links tick it. Held offers count towards session capacity.
- At the door: the Select items pop-up has the session picker, with
today's session preselected for new attendees.
- On pick-sessions events every ticket shows the session list straight
away. "All sessions" tickets record the ticked sessions
(RegistrationOption.sessionIds) at one price; scanning, attendance,
certificates, reminders and capacity follow them.
- Fix: per-session stock limits weren't enforced (takenCount filtered
WaitlistEntry by a sessionId column that didn't exist, and the error
was swallowed), and the public event page lost its stock info.
- Fix: editing a door registration with picked sessions couldn't be saved.
- Fix: the offline-scanning service worker served /_next/static cache-first
forever, so dev browsers that had opened the scanner ran stale code.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Event wizard: how people book (whole course / pick sessions), capacity
per session and per-option session mode. Session picker on the public
registration page, Manual registration and the kiosk; at the door points
to Manual registration. Register, tickets and My Events handle picked
sessions. Adds docs/ROADMAP-v2.md and docs/HANDOVER.md so work can carry
on from another machine, and a pick-sessions e2e test.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Timed jobs move from in-process timers to a job registry run by pg-boss:
once per schedule across processes, retries, run history, cron schedules
in the church timezone, and an admin Background jobs page with Run now.
Playwright end-to-end tests run against their own seeded database. A
Gitea Actions workflow runs backend tests, frontend checks and the e2e
tests, and deploys v* tags with scripts/deploy.sh once secrets are set.
Also fixes the at-the-door check-in count for multi-day tickets.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Event wizard: one date or multiple sessions with a repeat-pattern builder,
and a new Attendance step (late joining and pricing, check-in window,
session reminders, certificates with preview). Public pages show the
schedule and late prices; the scanner picks today's session; new
attendance register page, My Certificates, and a public verify page.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Event cards on the home page and events list get a diagonal "Early bird"
ribbon in the top-right corner while an early-bird tier is running and
registration is open. GET /api/events returns earlyBirdEndsAt per event.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Event pages emit schema.org Event JSON-LD (dates, venue, image,
organizer, ZAR offers, sold-out status) for Google event results.
- Event pages, the events list and home page set a canonical URL so
tagged and alias links don't split rankings.
- Funnel tracking falls back to the referring site (Facebook, Instagram,
Google, ...) and fbclid/gclid when a link has no utm_source/src.
- Share and Save QR on the event page tag their links as share / qr.
- getServerSettings moves to lib/serverSettings.ts for reuse.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The emailed/WhatsApp ticket PDF and the staff and at-the-door print
views now match the user dashboard's printed tickets: rounded cards with
a light border and soft shadow. Printing goes through a shared helper in
lib/ticketPrint.ts, which also escapes names in the print HTML.
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>
Cards instead of the table, phone-friendly sheets for the form and the uses list,
inactive codes hidden by default, and the events picker shows upcoming events only
(with search and a Show past events option).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Bundles get a kind: exact (as before), mix (any setSize of the qualifying tickets for
a set price), free (cheapest freeCount in each set free) and group (setSize or more
qualifying tickets at a per-ticket price or % off, applied once). The supervisor page
has a type picker, buyers see a one-line description of each deal, and the Complete
bundle cards handle every type. Demo: Family Fun Day adds a Lunch for four deal.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The quantity input had both w-full and w-16; w-full won, filling the row and pushing
the ticket picker off the side.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Upcoming events only by default (with a Show past events option), bundles as cards
instead of a table, and a mobile-friendly edit form.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Lists the event's deals and shows the Complete bundle / + cards, as at the door and
on the kiosk. Part of 1.28.1.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Both list the event's deals and show the Complete bundle / + cards from the register
form. At the door gets the standard Back to dashboard link.
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>
A bank charge on a cash deposit comes out of cash that was counted and
banked, so cash on hand is now the count less what reached the bank and
any bank-charge write-off. Before, a deposit fee showed as money "more
than expected".
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A full cashup now records, per payment method, how much of the expected
takings is in the bank, still on hand (cash) or pending (on the card
report or with a POP), and writes off what was lost to card fees, bank
charges or a shortfall. Whatever is left is shown as unaccounted; closing
with an unaccounted amount is allowed after a warning.
Pending money can be marked received or written off after the event is
closed, without reopening it. Reopening starts the next cashup from the
last one. Write-offs come off net profit and show as "Bank & card
charges" in the cashup, finance and profit reports.
The reports page also moves to the shared back link.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Every dashboard page now uses the shared BackLink, the small
"← Back to dashboard" link above the page title that the Waitlist page
already had, instead of a mix of grey Back buttons in different places.
Pages that had no way back (site settings, profile, donate, manual
registration) get one too.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
ReportsV2 gets typed API responses (funnel, feedback summary, coupon
redemptions, cashup audit, consent) and derived rows. Master-order
quantities move into a per-row map, so an option name can no longer
overwrite a fixed column. Consent row types move to src/types. Removes
unused export helpers and state.
With the frontend now at zero lint errors and warnings, next build runs
ESLint again (eslint.ignoreDuringBuilds is gone).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Each door panel and modal gets typed props; unused setError/setInfo/
onCreated props are dropped. The current early-bird price helpers move
to src/lib/pricing.ts (shared with manual registration), dead fallbacks
for a legacy registration 'options' field are removed, and the refund
history now shows a refund's reason (stored in its status field) instead
of a 'reason' field that never exists.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Moves the duplicated registration-option pricing helper into
src/lib/pricing.ts and drops an unused users list from payments.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>