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
29
README.md
29
README.md
|
|
@ -3,7 +3,10 @@
|
|||
End-to-end ticketing for the 2026 event:
|
||||
|
||||
1. **Purchase** — a FluentForms checkout on `tickets.beartariacampgrounds.com` POSTs a webhook to this backend, which writes a row to NocoDB, generates a unique ticket **QR code**, and emails it to the buyer via MailerSend (subject *"2026 Beartaria Campgrounds Tickets"*).
|
||||
2. **Gate** — staff scan the QR with **Camp Scan** (Android app + iPhone PWA, one Expo codebase). It validates the code, lets staff check in however many people are arriving on that visit, decrements the remaining count in NocoDB, and flashes **green + chime** / **red + buzz** with the name, counts, extras (parking/ice), and DB-update confirmation.
|
||||
2. **Gate** — staff scan the QR with **Camp Scan** (Android app + iPhone PWA, one Expo codebase). It flashes **green + chime** / **red + buzz** with name, counts, extras, and DB-update confirmation. Three scan modes:
|
||||
- **Check-in** — check in however many people are arriving on that visit; decrements the ticket count.
|
||||
- **Ice** — hand out prepaid ice bags (all at once or some now); decrements a separate ice count.
|
||||
- **Banquet** — show the scanned buyer's total donations (online + offline), looked up by their email; also supports a manual email lookup.
|
||||
3. **Admin** — a panel in the same app for manual lookup by name/email/code and button-based check-in/undo when scanning fails.
|
||||
|
||||
One QR per purchase is **reusable across visits** until all its tickets are redeemed (e.g. a family arriving in two groups on one code). Children under 4 are free and not counted.
|
||||
|
|
@ -45,8 +48,10 @@ The app expects the **2026 Campground Tickets** table to be a clone of the 2025
|
|||
| `Redeemed` | Number (default 0) |
|
||||
| `SubmissionKey` | SingleLineText |
|
||||
| `LastScanAt` | DateTime |
|
||||
| `Ice Total` | Number (prepaid ice bags) |
|
||||
| `Ice Redeemed` | Number (default 0) |
|
||||
|
||||
Total redeemable tickets = sum of the age-bracket columns **excluding `Ages 0-3`** (free). Column names are mapped in [`backend/src/fields.ts`](./backend/src/fields.ts) — change them there if the real titles differ. Put the table's ID (right-click table → *Copy Table ID*) in `NOCODB_TABLE_ID`.
|
||||
Total redeemable tickets = sum of the age-bracket columns **excluding `Ages 0-3`** (free). Ice bags remaining = `Ice Total − Ice Redeemed`. Column names are mapped in [`backend/src/fields.ts`](./backend/src/fields.ts) — change them there if the real titles differ. Put the table's ID (right-click table → *Copy Table ID*) in `NOCODB_TABLE_ID`.
|
||||
|
||||
### Audit log table — "2026 Ticket Audit Logs"
|
||||
|
||||
|
|
@ -62,7 +67,19 @@ Every check-in and undo is recorded to a separate table so the crew can review w
|
|||
| `Name` | SingleLineText |
|
||||
| `Remaining After` | Number |
|
||||
|
||||
Put its table ID in `NOCODB_AUDIT_TABLE_ID`. Leave the var empty to disable audit logging (check-ins still work). The admin panel shows global recent activity and per-ticket history from this table.
|
||||
Put its table ID in `NOCODB_AUDIT_TABLE_ID`. Leave the var empty to disable audit logging (check-ins still work). The admin panel shows global recent activity and per-ticket history from this table. Ice pickups are logged too (actions `ice` / `ice-undo`).
|
||||
|
||||
### Banquet mode — donor tables
|
||||
|
||||
Banquet mode reads existing donor tables (no new columns). Set these table IDs:
|
||||
|
||||
| Var | Table |
|
||||
|---|---|
|
||||
| `NOCODB_DONORS_TABLE_ID` | Donors Master List (authoritative `Total Donations` / `Total Online Donations` / `Total Offline Donations`, matched by `Email` or `Alternate Email`) |
|
||||
| `NOCODB_DONOR_ONLINE_TABLE_ID` | Donor Online Transactions (fallback sum of `Donation Amount` by email) |
|
||||
| `NOCODB_DONOR_OFFLINE_TABLE_ID` | Donor Offline Transactions (fallback) |
|
||||
|
||||
If `NOCODB_DONORS_TABLE_ID` is unset, banquet mode is disabled. Column names are mapped in [`backend/src/services/donors.ts`](./backend/src/services/donors.ts).
|
||||
|
||||
> `CampTickets TEST` and `CampTickets Audit TEST` tables already exist in NocoDB for testing. Point `NOCODB_TABLE_ID` / `NOCODB_AUDIT_TABLE_ID` at them for dry runs, then switch to the real 2026 tables for production.
|
||||
|
||||
|
|
@ -150,6 +167,7 @@ On the ticket form: **Settings & Integrations → Webhook → Add Webhook**.
|
|||
| `submission_id` | the entry/submission ID (for idempotency; content-hash fallback if omitted) |
|
||||
| `ages_0_3`, `ages_4_7`, `ages_8_12`, `ages_13_17`, `ages_18_25`, `ages_26_45`, `ages_46_64`, `ages_65` | headcount per bracket |
|
||||
| `car_parking`, `rv_parking`, `ice_access`, `is_donor` | yes/no or 1/0 |
|
||||
| `ice_bags` | prepaid ice bag count (optional; if omitted and `ice_access` is truthy, defaults to `ICE_BAGS_DEFAULT`) |
|
||||
| `address`, `payment_method` | optional |
|
||||
|
||||
On success the buyer receives the QR email. Re-sends of the same submission are idempotent (no duplicate rows/emails). If an email fails, the row is still created and returns HTTP 502 (visible in FluentForms' log); re-send later with `POST /api/tickets/{code}/resend-email` (staff-auth'd).
|
||||
|
|
@ -174,8 +192,9 @@ The runner runs jobs in a `node:22-bookworm` container and installs the Android
|
|||
|---|---|
|
||||
| `POST /api/auth/login` `{pin}` | Exchange PIN for a token |
|
||||
| `POST /webhook` (secret header) | FluentForms purchase → create ticket + email |
|
||||
| `POST /api/lookup` `{code}` | Read a ticket by code (no mutation) |
|
||||
| `POST /api/redeem` `{code, count}` | Check in `count` people (negative undoes); serialized per code |
|
||||
| `POST /api/lookup` `{code}` | Read a ticket by code (includes ice counts; no mutation) |
|
||||
| `POST /api/redeem` `{code, count, resource?}` | Redeem `count` of `resource` (`tickets` default, or `ice`); negative undoes; serialized per code+resource |
|
||||
| `POST /api/banquet` `{code?\|email?}` | Donor total for a scanned ticket's email or a manual email |
|
||||
| `GET /api/tickets?q=` | Search by name/email or exact code |
|
||||
| `GET /api/audit?code=&limit=` | Recent check-in log (all, or one code) |
|
||||
| `POST /api/tickets/{code}/resend-email` | Re-send the QR email |
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue