Fix ice bag count + default ice check-in to 1 bag

The form sends payment_ice as a descriptive label, e.g. "One Ice
ticket good for one bag per day (3 total bags)". The old parser pulled
the first number ("3") and treated it as 3 tickets, then multiplied by
3 bags/ticket → 9 bags for one ice ticket (18 for two). New
iceBagsFromPayment reads the "(N total bags)" the label states
directly, with worded-count and numeric dollar/count fallbacks for
forward compatibility. 1 ice → 3 bags, 2 → 6. 5 new tests.

Scanner: Ice mode now defaults the check-in count to 1 (a bag at a
time) instead of all remaining bags; staff can bump it up.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Hank 2026-07-20 07:40:05 +00:00
parent 49e45ff94c
commit 049f433930
4 changed files with 64 additions and 9 deletions

View file

@ -56,6 +56,34 @@ export function addressLine(v: any): string | undefined {
return undefined;
}
const NUMBER_WORDS: Record<string, number> = { one: 1, two: 2, three: 3, four: 4, five: 5, six: 6 };
/**
* Total bags of ice from the `payment_ice` field. The form sends a descriptive
* option label, e.g. "One Ice ticket good for one bag per day (3 total bags)",
* so the reliable signal is the "(N total bags)" the label states. Falls back to
* a worded ticket count ("Two Ice tickets" 2 × bagsPerTicket), then to a
* numeric dollar-total/ticket-count for forward compatibility.
*/
export function iceBagsFromPayment(
value: unknown,
opts: { bagsPerTicket: number; ticketPrice: number },
): number {
const { bagsPerTicket, ticketPrice } = opts;
const s = typeof value === "string" ? value : "";
// Preferred: the label states the total bags directly.
const bagsMatch = s.match(/(\d+)\s*total\s*bags/i);
if (bagsMatch) return Math.max(0, parseInt(bagsMatch[1], 10));
// Worded ticket count: "One Ice ticket", "Two Ice tickets".
const wordMatch = s.match(/\b(one|two|three|four|five|six)\b\s+ice/i);
if (wordMatch) return NUMBER_WORDS[wordMatch[1].toLowerCase()] * bagsPerTicket;
// Numeric fallback: a dollar total (>= price) → tickets; else a small count.
const n = qty(value);
if (n <= 0) return 0;
const tickets = n >= ticketPrice ? Math.round(n / ticketPrice) : Math.round(n);
return Math.max(0, tickets) * bagsPerTicket;
}
/** Donor status from the hidden lookup fields + the "are you a donor?" radio. */
export function readDonor(body: Record<string, any>): { isDonor: boolean; donorTier: string } {
const donorTier = String(body.donor_tier ?? "").trim();