diff --git a/README.md b/README.md index a3b8964..9aa9646 100644 --- a/README.md +++ b/README.md @@ -51,7 +51,9 @@ The app expects the **2026 Campground Tickets** table to be a clone of the 2025 | `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). 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`. +Total redeemable tickets = sum of the age-bracket columns **excluding `Ages 0-3`** (free). Ice bags remaining = `Ice Total − Ice Redeemed`. + +> **The table MUST have an `Id` primary key.** NocoDB's v2 `PATCH /records` with no primary key updates *every row in the table*, so a PK-less table would make each scan rewrite all tickets. The backend now refuses to update a record with no `Id` (fail-safe), but the table itself must have one. Tables cloned from the existing 2025 table already have `Id`; if you build one by hand via the API, include an `{"title":"Id","uidt":"ID"}` column. 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" diff --git a/backend/src/routes/test.ts b/backend/src/routes/test.ts index 3d4a680..4e51d1e 100644 --- a/backend/src/routes/test.ts +++ b/backend/src/routes/test.ts @@ -36,13 +36,14 @@ const PERSONAS: Persona[] = [ "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.", }, { - key: "donor", - name: "Adam Stevens (real donor)", - email: "adam21stevens@gmail.com", + key: "donor2", + name: "Donor Dan", + email: "donor@example.test", ages: { "Ages 26-45": 2 }, rvParking: true, isDonor: true, - blurb: "2 tickets, RV parking. Banquet mode → shows real donation total ($801).", + 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.", }, { key: "ice", diff --git a/backend/src/services/nocodb.ts b/backend/src/services/nocodb.ts index 2378b24..707a70a 100644 --- a/backend/src/services/nocodb.ts +++ b/backend/src/services/nocodb.ts @@ -86,6 +86,15 @@ export class NocoDBClient { /** Patch fields on a record identified by its NocoDB Id. */ async update(id: number, fields: Record): Promise { + // Fail safe: without a valid primary key, a v2 PATCH /records applies to + // EVERY row in the table. Refuse rather than mass-corrupt ticket counts. + // (A table missing its Id column will trip this — use a table with a PK.) + if (id === undefined || id === null || (typeof id === "number" && !Number.isFinite(id))) { + throw new NocoDBError( + "record has no Id — refusing to update (the table is missing its primary key)", + 500, + ); + } const body = await this.request(this.recordsUrl, { method: "PATCH", body: JSON.stringify({ Id: id, ...fields }), diff --git a/docs/fluentforms-donor-discount.md b/docs/fluentforms-donor-discount.md index 0fe77d5..dcec3fe 100644 --- a/docs/fluentforms-donor-discount.md +++ b/docs/fluentforms-donor-discount.md @@ -116,8 +116,9 @@ by FluentForms conditional logic based on its value. ## Test ``` -curl "https://scan.beartariacampgrounds.com/api/public/donor-eligibility?key=&email=adam21stevens@gmail.com" -# -> {"eligible":true,"tier":"member"} +curl "https://scan.beartariacampgrounds.com/api/public/donor-eligibility?key=&email=" +# donor/member -> {"eligible":true,"tier":"member"} +# anyone else -> {"eligible":false,"tier":null} ``` If member and donor get the **same** discounted price, simplify: set the donor