Vendor webhooks, free kids through 12, multi-origin lookup CORS

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>
This commit is contained in:
Hank 2026-07-16 05:13:07 +00:00
parent 43fddec286
commit 7296555964
15 changed files with 368 additions and 91 deletions

View file

@ -209,7 +209,7 @@ function TicketCard({ ticket, onAdjust }: { ticket: TicketView; onAdjust: (t: Ti
if (e.rvParking) tags.push("🚐 RV");
if (e.utv) tags.push("🏍️ UTV");
if (e.iceAccess || ticket.ice.total > 0) tags.push(`🧊 ${ticket.ice.remaining}/${ticket.ice.total}`);
if (e.freeUnder5 > 0) tags.push(`👶 ${e.freeUnder5} free`);
if (e.freeKids > 0) tags.push(`👶 ${e.freeKids} free kids`);
return (
<View style={styles.card}>

View file

@ -346,7 +346,7 @@ function ExtrasRow({ ticket }: { ticket: TicketView }) {
if (e.rvParking) tags.push("🚐 RV parking");
if (e.utv) tags.push("🏍️ UTV/ATV");
if (e.iceAccess || ticket.ice.total > 0) tags.push(`🧊 ${ticket.ice.remaining}/${ticket.ice.total} ice`);
if (e.freeUnder5 > 0) tags.push(`👶 ${e.freeUnder5} under 5 (free)`);
if (e.freeKids > 0) tags.push(`👶 ${e.freeKids} ${e.freeKids === 1 ? "kid" : "kids"} 12 & under (free)`);
if (!tags.length) return null;
return (
<View style={styles.tags}>
@ -365,6 +365,8 @@ const TYPE_ICON: Record<string, string> = {
Performer: "🎭",
Volunteer: "🙌",
Speaker: "🎤",
"Food Vendor": "🍔",
Vendor: "🛒",
};
function TypeBadge({ type }: { type: string }) {

View file

@ -13,6 +13,8 @@ const TYPE_ICON: Record<string, string> = {
Performer: "🎭",
Volunteer: "🙌",
Speaker: "🎤",
"Food Vendor": "🍔",
Vendor: "🛒",
};
const MEDAL = ["🥇", "🥈", "🥉"];
@ -138,9 +140,9 @@ export default function StatsScreen() {
<View style={styles.card}>
<Text style={styles.cardTitle}>Who's coming</Text>
<View style={styles.tileRow}>
<Tile value={stats.people.adults} label="adults" />
<Tile value={stats.people.youth} label="youth 13-16" />
<Tile value={stats.people.kids12 + stats.people.kids9} label="kids 5-12" />
<Tile value={stats.people.adults} label="adults (paid)" />
<Tile value={stats.people.youth} label="youth 13-16 (paid)" />
<Tile value={stats.people.kids12 + stats.people.kids9} label="kids 5-12 (free)" />
<Tile value={stats.people.kids4Free} label="under 5 (free)" />
</View>
</View>

View file

@ -36,7 +36,7 @@ export interface TicketView {
isDonor: boolean;
donorTier: string;
vouchers: number;
freeUnder5: number;
freeKids: number;
};
ages: { bracket: string; count: number; free: boolean }[];
}