Two corrections to the adult-ticket model:
1. Adult total was undercounting. Voucher (donor) tickets live only in
the names_Donor_1 / names_Donor_2 name fields — each filled name is
one free voucher adult ticket — and weren't counted at all. Adults
now = item_quantity_adult_ticket_reg (regular) +
item_quantity_adult_ticket_donor (extra PAID donor tickets beyond
vouchers) + the donor voucher-name count. Vouchers consumed is now
that same donor-name count (what the ticket-voucher lookup subtracts),
instead of the hidden `vouchers` entitlement.
2. Ticket title now comes from customer_name (billing name), not the
first adult ticket name.
Doc updated to describe the adult total and the title source.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Two fixes for the updated Tickets 2026 form:
1. Purchaser name now comes from the new customer_name (billing) field,
falling back to the first attendee then a plain `name`. Donor-only
and buy-for-others orders (where the Adult #1 `names` group is empty)
no longer 400 with "purchaser name is required". The ticket title is
the first attendee if present, else the customer; the QR email is
addressed to the customer.
2. Tickets are now optional. A customer can buy ice / ATV-UTV / parking
with no admission ticket. A record + QR is created whenever there's
anything to redeem or verify at the gate (ticket, ice, or add-on);
only a truly empty order is rejected (no_items, replacing no_tickets).
The ticket email adapts its copy for ticketless (add-on-only) orders —
it reads as a gate pass for ice/parking/UTV instead of "0 tickets", and
names the ice bag count when present. Idempotency hash now includes
ice/extras so distinct add-on-only orders don't collide. Doc updated.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The updated Tickets 2026 form adds two donor (voucher) adult-ticket
name groups, names_Donor_1 / names_Donor_2, for the free adult
admissions. These were already counted via
item_quantity_adult_ticket_donor but their attendee names weren't
captured — added them to the adult-name list so they show at the gate.
Doc updated (new name fields + note that pure pricing line items and
payment_donor_voucher1/2 are ignored; the vouchers hidden count is
authoritative). No other schema changes needed — counts, extras,
donor, ice, and voucher handling already matched.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The ticket-vouchers lookup previously returned the tier entitlement
every time, so a donor could keep claiming free tickets by re-
submitting the form. It now subtracts vouchers already consumed:
remaining = entitled - used
where `used` is the sum of the Vouchers column across that donor's
prior ticket orders (each checkout stores what it applied). Response
gains entitled/used/remaining; `vouchers` is now the remaining count
the form should grant. Consumption is implicit — no counter to keep in
sync — and resets by zeroing/deleting the Vouchers value on the order
row in NocoDB.
- nocodb: findByEmail + vouchersUsedByEmail (case-insensitive).
- 8 new tests (36 total). Doc updated with the new response + reset.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Only food vendors receive gate passes. The /vendor-webhook/non-food
endpoint now acknowledges the submission ({"status":"ignored"}) and
issues nothing, instead of creating a 1-pass ticket — kept as a safe
no-op so an accidentally-wired FluentForms feed doesn't 404. Food
webhook unchanged. Doc + tests updated.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Children now free through age 12:
- Scannable/paid ticket total = adults + youth 13-16 only; kids 12 &
under (0-4, 5-9, 10-12) are stored but not counted (charging starts
at 13). computeTotal + freeKidsCount in fields.ts, webhook guard,
scan/admin badges, event-report labels, docs, and personas updated.
Vendor booth webhooks (vendors.beartariacampgrounds.com):
- New /vendor-webhook/food (2 named pass-holders) and
/vendor-webhook/non-food (1 pass-holder), reusing WEBHOOK_SECRET.
Booth name -> ticket title; each named person = one entry pass;
tagged with a "Food Vendor"/"Vendor" Ticket Type (badge on scan +
event-report rollup). Idempotent + QR email like the attendee hook.
- Extracted shared FluentForms parsing (nameGroup/qty/selected/
addressLine/readDonor) into fluentforms.ts; attendee webhook now
imports it. 13 new unit tests.
Public lookup CORS is now a comma-separated allowlist; the caller's
Origin is echoed only if it matches. tickets + vendors both allowed
on donor-eligibility and ticket-vouchers.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Reporting: GET /api/stats aggregates check-in progress, ice, ticket types,
people breakdown, add-ons/donors, gate-crew leaderboard (from audit),
comp tickets by creator, and a by-hour check-in timeline. New /stats screen.
- Slide-out drawer (custom RN Animated, no new native deps) replaces per-screen
header links; available on every main screen via a hamburger.
- In-app comp portal (/comp), password-gated like /crush33, reusing the portal
endpoints; records the issuing gate-staff name (Created By column) and reports
comps per creator.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Portal (/crush33): password-gated page (PORTAL_PASSWORD) for admins to create
entry-only tickets from just name + email, with a category
(Guest/Worker/Performer/Volunteer/Speaker). Creates a 1-admission ticket, emails
the QR, and shows the QR on-screen. New Ticket Type column.
Scanner: shows a prominent TYPE badge (🎭 PERFORMER, 🛠️ WORKER, …) on the
confirm + success screens and in admin, so staff can see it's a special ticket.
Added Worker + Performer personas to /test.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
New form schema: up to 10 named adults, youth 13-16, kids 10-12 / 5-9 / 0-4,
donor tier/vouchers (from the lookups), parking/RV/UTV/ice payment fields.
- Scannable total = adults + youth + kids 10-12 + kids 5-9 (kids 0-4 free).
- Store + display adult names on a good scan; show donor tier, UTV, vouchers.
- Ice: payment_ice = 1-4 tickets ($20 each), 1 ticket = 3 bags.
- New NocoDB 2026 schema (with Id PK); webhook parses compound names,
quantity/payment fields (nested objects or money strings).
- Our webhook keeps sending the QR ticket email (FluentForms sends the receipt);
from address is now info@beartariacampgrounds.com.
Updated /test personas, /webhook-doc, tests, and the app display.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
GET /api/public/ticket-vouchers?key=&email= returns 0/1/2 free tickets based on
donations on/after VOUCHER_SINCE (default 2025-09-04): >= $400 -> 1, >= $1000 -> 2.
Same secret/CORS/rate-limit as donor-eligibility; returns only the count. Cutoff
and thresholds are env-configurable. Documented both lookup APIs (discount +
vouchers) in docs/fluentforms-donor-discount.md with form snippets.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Critical: NocoDB v2 PATCH /records with no primary key updates EVERY row, so a
table missing its Id column made each redeem rewrite all tickets' counts.
- nocodb update() now refuses to PATCH a record with no Id (fail-safe).
- Document that the tickets/audit tables must have an Id PK; note how to add it.
- Replace the real donor email/name used in the /test persona and the
FluentForms guide with fake placeholders (donor@example.test / <a-real-donor-email>).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
GET /api/public/donor-eligibility?key=&email= returns only {eligible, tier}
(member/donor) — no names or dollar amounts — gated by PUBLIC_LOOKUP_SECRET,
rate-limited (30/min), and CORS-restricted to PUBLIC_LOOKUP_ORIGIN. Lets the
FluentForms checkout unlock a donor discount by email. Docs + ready-to-paste
form snippet in docs/fluentforms-donor-discount.md.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Served at /webhook-doc: endpoint + auth header, full field table (name/email,
age brackets, ice, parking, donor, idempotency key), example JSON + curl,
response codes, and FluentForms feed setup steps.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Served unauthenticated at /install: one-tap "Add to Obtainium" deep link,
how to get Obtainium, direct-APK fallback to the Forgejo releases, and iPhone
Add-to-Home-Screen steps. Detects the visitor's platform and shows it first.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Login: fixed-length 4-digit PIN that auto-submits on the 4th digit (no submit
button to scroll to on small iPhone screens) and clears on a wrong PIN.
Compact, vertically-centered keypad so it fits without scrolling.
Operator tracking: after PIN auth, staff enter their name (new /operator
screen, persisted per device). The name is sent as X-Operator on every authed
request and recorded on each check-in/undo/ice audit entry (new Operator
column), so logs show who did what. Shown in the scanner header and the admin
audit view.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- Auth: introduce a shared AuthProvider/useAuth so entering the PIN updates
reactive state and the layout's gate navigates immediately (previously it
cached the token check at startup, so login appeared to hang until a manual
refresh). login/scanner now use signIn/signOut.
- Banquet: donor lookup now returns lifetime giving, last-12-months giving
(computed from dated transactions), member/donor status, bear name, and tags.
The banquet result screen shows status badge + lifetime and last-year figures.
Tickets are irrelevant in banquet mode.
- Admin: fix "‹ Scanner" back link wrapping (remove fixed width).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
GET /test (gated by ENABLE_TEST_PAGE) renders scannable QR codes for a set of
personas covering different attributes — solo, family with ice+parking, a real
donor for Banquet mode, ice-only, a pre-exhausted ticket, and an invalid code.
Idempotently seeds them into the current NocoDB table.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- Ice mode: prepaid ice bags (Ice Total / Ice Redeemed columns) redeemed
independently of ticket check-ins; grab all bags at once or some now.
- Banquet mode: donor total (online + offline) looked up by the ticket's
email via the Donors Master List, with a manual email override. New
DonorService + POST /api/banquet.
- Redeem generalized over a resource (tickets|ice); audit records ice actions.
- App gains a mode selector; webhook maps ice_bags (defaults to
ICE_BAGS_DEFAULT when only a boolean ice option is present).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Every successful check-in/undo writes a row to the "2026 Ticket Audit Logs"
NocoDB table (timestamp, people, code, action, name, remaining-after).
Non-fatal: audit failures never block a gate check-in. New GET /api/audit
endpoint (global or per-code). Admin panel gains a global "Recent check-ins"
panel and per-ticket history. Audit table is optional via NOCODB_AUDIT_TABLE_ID.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>