CampgroundTickets/docs/fluentforms-ticket-vouchers.md
Hank 1ba3f9ad1c Ticket vouchers now return remaining and decrement on use
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>
2026-07-16 22:11:08 +00:00

131 lines
5.1 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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).