Rework webhook + model for the 2026 Tickets form
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>
This commit is contained in:
parent
718d1515b0
commit
b7c77afe02
14 changed files with 360 additions and 210 deletions
|
|
@ -7,57 +7,68 @@ interface Persona {
|
|||
key: string;
|
||||
name: string;
|
||||
email: string;
|
||||
ages: Record<string, number>;
|
||||
adultNames?: string[];
|
||||
counts: { adults: number; youth: number; kids12: number; kids9: number; kids4: number };
|
||||
iceBags?: number;
|
||||
carParking?: boolean;
|
||||
rvParking?: boolean;
|
||||
utv?: boolean;
|
||||
isDonor?: boolean;
|
||||
donorTier?: string;
|
||||
exhaust?: boolean; // pre-redeem all tickets so it scans as "exhausted"
|
||||
blurb: string;
|
||||
}
|
||||
|
||||
const C = (adults = 0, youth = 0, kids12 = 0, kids9 = 0, kids4 = 0) => ({ adults, youth, kids12, kids9, kids4 });
|
||||
|
||||
// A curated set covering the different attribute combinations to test.
|
||||
const PERSONAS: Persona[] = [
|
||||
{
|
||||
key: "solo",
|
||||
name: "Solo Sam",
|
||||
email: "solo@test.beartaria",
|
||||
ages: { "Ages 18-25": 1 },
|
||||
adultNames: ["Solo Sam"],
|
||||
counts: C(1),
|
||||
blurb: "1 ticket, no extras. Check-in mode → green, 1/1.",
|
||||
},
|
||||
{
|
||||
key: "family",
|
||||
name: "Family Fay",
|
||||
email: "family@test.beartaria",
|
||||
ages: { "Ages 0-3": 2, "Ages 8-12": 3, "Ages 26-45": 2 },
|
||||
adultNames: ["Family Fay", "Frank Fay"],
|
||||
counts: C(2, 0, 0, 3, 2), // 2 adults + 3 kids(5-9) = 5 scannable; 2 kids 0-4 free
|
||||
iceBags: 3,
|
||||
carParking: true,
|
||||
blurb:
|
||||
"5 tickets (2 under-4 free), car parking, 3 ice bags. Check-in a few at a time to test QR reuse; then Ice mode.",
|
||||
"5 tickets (2 adults + 3 kids 5-9; two 0-4 free), car parking, 3 ice bags. Check-in a few at a time to test QR reuse + see adult names; then Ice mode.",
|
||||
},
|
||||
{
|
||||
key: "donor2",
|
||||
name: "Donor Dan",
|
||||
email: "donor@example.test",
|
||||
ages: { "Ages 26-45": 2 },
|
||||
adultNames: ["Donor Dan", "Donna Dan"],
|
||||
counts: C(2),
|
||||
rvParking: true,
|
||||
utv: true,
|
||||
isDonor: true,
|
||||
donorTier: "member",
|
||||
blurb:
|
||||
"2 tickets, RV parking, donor-flagged. Banquet mode: this test email has no real donations, so use Banquet's manual email lookup with a real donor's address to see totals.",
|
||||
"2 tickets, RV + UTV, donor/member. Banquet mode: this test email has no real donations — use Banquet's manual email lookup with a real donor's address.",
|
||||
},
|
||||
{
|
||||
key: "ice",
|
||||
name: "Ice Ike",
|
||||
email: "ice@test.beartaria",
|
||||
ages: { "Ages 18-25": 1 },
|
||||
iceBags: 3,
|
||||
blurb: "1 ticket + 3 ice bags. Ice mode → grab all 3 at once, then scan again → exhausted.",
|
||||
adultNames: ["Ice Ike"],
|
||||
counts: C(1),
|
||||
iceBags: 6, // 2 ice tickets
|
||||
blurb: "1 ticket + 6 ice bags (2 ice tickets). Ice mode → grab bags, then scan again → exhausted.",
|
||||
},
|
||||
{
|
||||
key: "exhausted",
|
||||
name: "Done Dora",
|
||||
email: "done@test.beartaria",
|
||||
ages: { "Ages 26-45": 2 },
|
||||
counts: C(2),
|
||||
exhaust: true,
|
||||
blurb: "2 tickets, already fully redeemed. Check-in mode → red 'exhausted'.",
|
||||
},
|
||||
|
|
@ -74,22 +85,22 @@ export async function testRoutes(app: FastifyInstance): Promise<void> {
|
|||
for (const p of PERSONAS) {
|
||||
const result = await createTicket(app.ctx, {
|
||||
name: p.name,
|
||||
adultNames: p.adultNames,
|
||||
email: p.email,
|
||||
ages: p.ages,
|
||||
counts: p.counts,
|
||||
iceBags: p.iceBags,
|
||||
carParking: p.carParking,
|
||||
rvParking: p.rvParking,
|
||||
utv: p.utv,
|
||||
isDonor: p.isDonor,
|
||||
donorTier: p.donorTier,
|
||||
submissionKey: `test:${p.key}`,
|
||||
});
|
||||
// Keep the "exhausted" persona fully redeemed on every load so its state
|
||||
// is deterministic (compute the total from the persona's own age counts,
|
||||
// since NocoDB's create response may not echo them back).
|
||||
// is deterministic (total = scannable count from the persona's counts).
|
||||
if (p.exhaust) {
|
||||
const total = Object.entries(p.ages)
|
||||
.filter(([col]) => col !== "Ages 0-3")
|
||||
.reduce((s, [, n]) => s + n, 0);
|
||||
await app.ctx.nocodb.update(result.record.Id, { [COL.redeemed]: total });
|
||||
const { adults, youth, kids12, kids9 } = p.counts;
|
||||
await app.ctx.nocodb.update(result.record.Id, { [COL.redeemed]: adults + youth + kids12 + kids9 });
|
||||
}
|
||||
cards.push({
|
||||
code: result.code,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue