Fix data-corruption on PK-less table; scrub real emails from examples

Critical: NocoDB v2 PATCH /records with no primary key updates EVERY row, so a
table missing its Id column made each redeem rewrite all tickets' counts.
- nocodb update() now refuses to PATCH a record with no Id (fail-safe).
- Document that the tickets/audit tables must have an Id PK; note how to add it.
- Replace the real donor email/name used in the /test persona and the
  FluentForms guide with fake placeholders (donor@example.test / <a-real-donor-email>).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Hank 2026-07-08 22:27:09 +00:00
parent ba5cb8a9fb
commit 65aa878192
4 changed files with 20 additions and 7 deletions

View file

@ -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"

View file

@ -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",

View file

@ -86,6 +86,15 @@ export class NocoDBClient {
/** Patch fields on a record identified by its NocoDB Id. */
async update(id: number, fields: Record<string, unknown>): Promise<NocoRecord> {
// 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 }),

View file

@ -116,8 +116,9 @@ by FluentForms conditional logic based on its value.
## Test
```
curl "https://scan.beartariacampgrounds.com/api/public/donor-eligibility?key=<SECRET>&email=adam21stevens@gmail.com"
# -> {"eligible":true,"tier":"member"}
curl "https://scan.beartariacampgrounds.com/api/public/donor-eligibility?key=<SECRET>&email=<a-real-donor-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