The ticket-vouchers lookup previously returned the tier entitlement every time, so a donor could keep claiming free tickets by re- submitting the form. It now subtracts vouchers already consumed: remaining = entitled - used where `used` is the sum of the Vouchers column across that donor's prior ticket orders (each checkout stores what it applied). Response gains entitled/used/remaining; `vouchers` is now the remaining count the form should grant. Consumption is implicit — no counter to keep in sync — and resets by zeroing/deleting the Vouchers value on the order row in NocoDB. - nocodb: findByEmail + vouchersUsedByEmail (case-insensitive). - 8 new tests (36 total). Doc updated with the new response + reset. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
131 lines
5.1 KiB
Markdown
131 lines
5.1 KiB
Markdown
# FluentForms → ticket-voucher lookup
|
||
|
||
Look up, by email, how many **free tickets** a donor has earned from their
|
||
giving. Intended for the ticket-rewards / checkout form: enter an email, call
|
||
this endpoint, and show / apply the earned vouchers.
|
||
|
||
FluentForms can't query an external database from a field natively, so this is
|
||
done with a small Custom JS block that calls a secret-gated endpoint on the
|
||
ticketing backend.
|
||
|
||
## Endpoint
|
||
|
||
```
|
||
GET https://scan.beartariacampgrounds.com/api/public/ticket-vouchers?key=<SECRET>&email=<email>
|
||
```
|
||
|
||
Returns the **remaining** free-ticket count — never names or dollar amounts:
|
||
|
||
```json
|
||
{ "vouchers": 1, "entitled": 2, "used": 1, "remaining": 1 }
|
||
```
|
||
|
||
- `vouchers` / `remaining` — how many free tickets are **still available** (this
|
||
is what the form should grant). Use `vouchers`; `remaining` is an alias.
|
||
- `entitled` — the tier entitlement earned from giving (0/1/2).
|
||
- `used` — vouchers already consumed by this donor's prior ticket orders.
|
||
- `key` = the value of `PUBLIC_LOOKUP_SECRET` (set in the backend `.env`).
|
||
- `email` = the donor's email (URL-encoded).
|
||
- Rate-limited (30 requests / minute / IP) and CORS-restricted to
|
||
`PUBLIC_LOOKUP_ORIGIN` (`tickets.` + `vendors.beartariacampgrounds.com`).
|
||
|
||
### Vouchers decrement as they're used
|
||
|
||
`remaining = entitled − used`, where `used` is the sum of the **Vouchers**
|
||
column across every ticket order placed with that email. Each checkout stores
|
||
the vouchers it applied, so the next lookup returns fewer — a donor can't keep
|
||
claiming free tickets by re-submitting the form. Once `used ≥ entitled`,
|
||
`vouchers` is `0`.
|
||
|
||
**To reset for testing:** in NocoDB, zero out (or delete) the **Vouchers**
|
||
value on that donor's ticket order row(s). `used` drops and `remaining` rises on
|
||
the next lookup — no redeploy needed.
|
||
|
||
> The secret is visible in page source, so treat it as **deterrence, not
|
||
> security** — it only gates a 0/1/2 count. Rotate it by changing
|
||
> `PUBLIC_LOOKUP_SECRET` and redeploying.
|
||
|
||
## Rules
|
||
|
||
Donations are summed for the email across the online + offline transaction
|
||
tables, counting only **Paid** rows dated **on/after `VOUCHER_SINCE`**:
|
||
|
||
| Total since the cutoff | Vouchers |
|
||
|---|---|
|
||
| ≥ $1000 | 2 |
|
||
| ≥ $400 | 1 |
|
||
| otherwise | 0 |
|
||
|
||
Configurable in the backend `.env`:
|
||
|
||
| Var | Default | Meaning |
|
||
|---|---|---|
|
||
| `VOUCHER_SINCE` | `2025-09-04` | Only donations on/after this date count. Bump each year. |
|
||
| `VOUCHER_TIER1_MIN` | `400` | Dollar total for 1 voucher |
|
||
| `VOUCHER_TIER2_MIN` | `1000` | Dollar total for 2 vouchers |
|
||
|
||
The count comes from the **dated transaction tables** (the donor master-list
|
||
rollups have no dates), so donations must exist in those tables for the window.
|
||
|
||
## Form snippet
|
||
|
||
Add a **Custom HTML** element to the form and paste this, setting `KEY` to your
|
||
`PUBLIC_LOOKUP_SECRET` (and `EMAIL_SELECTOR` if the email field isn't named
|
||
`email`). It shows the earned count on email blur and writes it into a hidden
|
||
field `free_tickets` you can use for conditional logic or to cap a quantity.
|
||
|
||
```html
|
||
<div id="voucher-status" style="margin:6px 0;font-size:14px;font-weight:600;"></div>
|
||
<script>
|
||
(function () {
|
||
var API = "https://scan.beartariacampgrounds.com/api/public/ticket-vouchers";
|
||
var KEY = "REPLACE_WITH_PUBLIC_LOOKUP_SECRET";
|
||
var EMAIL_SELECTOR = 'input[name="email"]';
|
||
var statusEl = document.getElementById("voucher-status");
|
||
var last = "";
|
||
|
||
function show(n) {
|
||
statusEl.textContent = n > 0
|
||
? "🎟️ You've earned " + n + " free ticket" + (n > 1 ? "s" : "") + "!"
|
||
: "";
|
||
statusEl.style.color = "#1b7f3b";
|
||
// Optional: write the count into a hidden field named "free_tickets".
|
||
// Uses the native setter so FluentForms' Vue model registers the change.
|
||
var el = document.querySelector('input[name="free_tickets"]');
|
||
if (el) {
|
||
var d = Object.getOwnPropertyDescriptor(Object.getPrototypeOf(el), "value");
|
||
(d && d.set ? d.set : function (v) { el.value = v; }).call(el, String(n));
|
||
el.dispatchEvent(new Event("input", { bubbles: true }));
|
||
el.dispatchEvent(new Event("change", { bubbles: true }));
|
||
}
|
||
}
|
||
|
||
function check(email) {
|
||
if (!email || email === last) return;
|
||
last = email;
|
||
fetch(API + "?key=" + encodeURIComponent(KEY) + "&email=" + encodeURIComponent(email))
|
||
.then(function (r) { return r.json(); })
|
||
.then(function (d) { show(d && d.vouchers ? d.vouchers : 0); })
|
||
.catch(function () { show(0); });
|
||
}
|
||
|
||
function bind() {
|
||
var el = document.querySelector(EMAIL_SELECTOR);
|
||
if (!el) { return setTimeout(bind, 500); } // form may render late
|
||
el.addEventListener("blur", function () { check(el.value.trim().toLowerCase()); });
|
||
}
|
||
bind();
|
||
})();
|
||
</script>
|
||
```
|
||
|
||
## Test
|
||
|
||
```
|
||
curl "https://scan.beartariacampgrounds.com/api/public/ticket-vouchers?key=<SECRET>&email=<a-real-donor-email>"
|
||
# entitled 2, none used yet -> {"vouchers":2,"entitled":2,"used":0,"remaining":2}
|
||
# after a checkout using 2 -> {"vouchers":0,"entitled":2,"used":2,"remaining":0}
|
||
```
|
||
|
||
Related: [`fluentforms-donor-discount.md`](./fluentforms-donor-discount.md) — the
|
||
companion donor-discount eligibility lookup (same key / CORS / rate limit).
|