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:
parent
ba5cb8a9fb
commit
65aa878192
4 changed files with 20 additions and 7 deletions
|
|
@ -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 Total` | Number (prepaid ice bags) |
|
||||||
| `Ice Redeemed` | Number (default 0) |
|
| `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"
|
### Audit log table — "2026 Ticket Audit Logs"
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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.",
|
"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",
|
key: "donor2",
|
||||||
name: "Adam Stevens (real donor)",
|
name: "Donor Dan",
|
||||||
email: "adam21stevens@gmail.com",
|
email: "donor@example.test",
|
||||||
ages: { "Ages 26-45": 2 },
|
ages: { "Ages 26-45": 2 },
|
||||||
rvParking: true,
|
rvParking: true,
|
||||||
isDonor: 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",
|
key: "ice",
|
||||||
|
|
|
||||||
|
|
@ -86,6 +86,15 @@ export class NocoDBClient {
|
||||||
|
|
||||||
/** Patch fields on a record identified by its NocoDB Id. */
|
/** Patch fields on a record identified by its NocoDB Id. */
|
||||||
async update(id: number, fields: Record<string, unknown>): Promise<NocoRecord> {
|
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, {
|
const body = await this.request(this.recordsUrl, {
|
||||||
method: "PATCH",
|
method: "PATCH",
|
||||||
body: JSON.stringify({ Id: id, ...fields }),
|
body: JSON.stringify({ Id: id, ...fields }),
|
||||||
|
|
|
||||||
|
|
@ -116,8 +116,9 @@ by FluentForms conditional logic based on its value.
|
||||||
## Test
|
## Test
|
||||||
|
|
||||||
```
|
```
|
||||||
curl "https://scan.beartariacampgrounds.com/api/public/donor-eligibility?key=<SECRET>&email=adam21stevens@gmail.com"
|
curl "https://scan.beartariacampgrounds.com/api/public/donor-eligibility?key=<SECRET>&email=<a-real-donor-email>"
|
||||||
# -> {"eligible":true,"tier":"member"}
|
# 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
|
If member and donor get the **same** discounted price, simplify: set the donor
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue