import type { FastifyInstance } from "fastify"; import { createTicket } from "../ticketService.js"; import { renderQrDataUrl } from "../services/qrcode.js"; import { COL } from "../fields.js"; interface Persona { key: string; name: string; email: string; 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", 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", 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 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", adultNames: ["Donor Dan", "Donna Dan"], counts: C(2), rvParking: true, utv: true, isDonor: true, donorTier: "member", blurb: "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", 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", counts: C(2), exhaust: true, blurb: "2 tickets, already fully redeemed. Check-in mode → red 'exhausted'.", }, ]; const INVALID_CODE = "BC26-0000-0000"; // not in the DB → scans as "not found" export async function testRoutes(app: FastifyInstance): Promise { if (!app.ctx.config.ENABLE_TEST_PAGE) return; app.get("/test", async (_req, reply) => { const cards: { code: string; qr: string; name: string; blurb: string }[] = []; for (const p of PERSONAS) { const result = await createTicket(app.ctx, { name: p.name, adultNames: p.adultNames, email: p.email, 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 (total = scannable count from the persona's counts). if (p.exhaust) { 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, qr: await renderQrDataUrl(result.code), name: p.name, blurb: p.blurb, }); } // An invalid one to test the not-found path. cards.push({ code: INVALID_CODE, qr: await renderQrDataUrl(INVALID_CODE), name: "Not a real ticket", blurb: "Any mode → red 'not a valid ticket'.", }); reply.type("text/html").send(renderPage(cards)); }); } function esc(s: string): string { return String(s).replace(/[&<>"]/g, (c) => c === "&" ? "&" : c === "<" ? "<" : c === ">" ? ">" : """, ); } function renderPage(cards: { code: string; qr: string; name: string; blurb: string }[]): string { const items = cards .map( (c) => `
QR for ${esc(c.code)}
${esc(c.name)}
${esc(c.code)}
${esc(c.blurb)}
`, ) .join(""); return ` Camp Scan — Test QR codes

🐻 Camp Scan — test tickets

Open the app on another device and scan these. Switch modes (Check-in / Ice / Banquet) to test each. Reload to re-seed if a record was deleted.

${items}
`; }