Add Check-in / Ice / Banquet scan modes
- 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>
This commit is contained in:
parent
b36d63a6a4
commit
d293e53ee6
15 changed files with 587 additions and 125 deletions
|
|
@ -76,13 +76,54 @@ export async function ticketRoutes(app: FastifyInstance): Promise<void> {
|
|||
properties: {
|
||||
code: { type: "string", minLength: 1, maxLength: 64 },
|
||||
count: { type: "integer", minimum: -100, maximum: 100 },
|
||||
resource: { type: "string", enum: ["tickets", "ice"] },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
async (req) => {
|
||||
const { code, count } = req.body as { code: string; count?: number };
|
||||
return redeem(app.ctx, normalizeCode(code), count ?? 1);
|
||||
const { code, count, resource } = req.body as {
|
||||
code: string;
|
||||
count?: number;
|
||||
resource?: "tickets" | "ice";
|
||||
};
|
||||
return redeem(app.ctx, normalizeCode(code), count ?? 1, resource ?? "tickets");
|
||||
},
|
||||
);
|
||||
|
||||
// Banquet mode: total donations for the email on a ticket (or a manual email).
|
||||
app.post(
|
||||
"/api/banquet",
|
||||
{
|
||||
preHandler: requireStaff,
|
||||
schema: {
|
||||
body: {
|
||||
type: "object",
|
||||
properties: {
|
||||
code: { type: "string", maxLength: 64 },
|
||||
email: { type: "string", maxLength: 200 },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
async (req, reply) => {
|
||||
if (!app.ctx.donors.enabled) {
|
||||
return reply.code(422).send({ ok: false, reason: "banquet_disabled" });
|
||||
}
|
||||
const { code, email } = req.body as { code?: string; email?: string };
|
||||
let lookupEmail = (email ?? "").trim();
|
||||
let ticketName = "";
|
||||
// If a ticket code was scanned, resolve its email.
|
||||
if (!lookupEmail && code) {
|
||||
const res = await lookupByCode(app.ctx, normalizeCode(code));
|
||||
if (!res.ok) return { ok: false, reason: "db_error", detail: res.detail };
|
||||
if (!res.found) return { ok: false, reason: "not_found" };
|
||||
lookupEmail = res.ticket.email;
|
||||
ticketName = res.ticket.name;
|
||||
}
|
||||
if (!lookupEmail) return { ok: false, reason: "no_email" };
|
||||
const donor = await app.ctx.donors.lookup(lookupEmail);
|
||||
return { ok: true, ticketName, donor };
|
||||
},
|
||||
);
|
||||
|
||||
|
|
|
|||
|
|
@ -60,6 +60,15 @@ export async function webhookRoutes(app: FastifyInstance): Promise<void> {
|
|||
.digest("hex")
|
||||
.slice(0, 32);
|
||||
|
||||
// Ice: prefer an explicit bag count; else grant the default when a boolean
|
||||
// ice option is truthy; else 0.
|
||||
let iceBags = 0;
|
||||
if (body.ice_bags !== undefined && body.ice_bags !== null && body.ice_bags !== "") {
|
||||
iceBags = toNumber(body.ice_bags);
|
||||
} else if (body.ice_access !== undefined && toBool(body.ice_access)) {
|
||||
iceBags = app.ctx.config.ICE_BAGS_DEFAULT;
|
||||
}
|
||||
|
||||
let result: Awaited<ReturnType<typeof createTicket>>;
|
||||
try {
|
||||
result = await createTicket(app.ctx, {
|
||||
|
|
@ -70,6 +79,7 @@ export async function webhookRoutes(app: FastifyInstance): Promise<void> {
|
|||
carParking: body.car_parking !== undefined ? toBool(body.car_parking) : undefined,
|
||||
rvParking: body.rv_parking !== undefined ? toBool(body.rv_parking) : undefined,
|
||||
iceAccess: body.ice_access !== undefined ? toBool(body.ice_access) : undefined,
|
||||
iceBags,
|
||||
paymentMethod: body.payment_method !== undefined ? String(body.payment_method) : undefined,
|
||||
ages,
|
||||
submissionKey,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue