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

5.1 KiB
Raw Blame History

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:

{ "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.

<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 — the companion donor-discount eligibility lookup (same key / CORS / rate limit).