CampgroundTickets/docs/fluentforms-donor-discount.md
Hank 718d1515b0 docs: dedicated ticket-voucher lookup page under fluent
Move the ticket-voucher API into its own standalone doc
(docs/fluentforms-ticket-vouchers.md) — endpoint, 0/1/2 rules, config, form
snippet, and curl test — so it's easy to find and hand off for the lookup. The
donor-discount doc now links to it instead of duplicating the section.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-10 06:36:39 +00:00

6 KiB

FluentForms → donor lookup APIs

FluentForms has no native way to query an external database from a field. These wire it up with a small Custom JS block that calls a secret-gated endpoint on the ticketing backend. Two endpoints are available (same key, CORS, and rate limit):

Endpoint Purpose
GET /api/public/donor-eligibility Is this email a donor/member? → unlock a discount
GET /api/public/ticket-vouchers How many free tickets has this donor earned? → 0 / 1 / 2

Both require ?key=<PUBLIC_LOOKUP_SECRET>, are rate-limited (30/min/IP), and CORS-restricted to PUBLIC_LOOKUP_ORIGIN (default https://tickets.beartariacampgrounds.com). Neither returns names or dollar amounts. The secret is visible in page source, so treat it as deterrence, not security; rotate it by changing PUBLIC_LOOKUP_SECRET and redeploying.


Donor discount lookup

Unlocks a discount when the entered email belongs to a donor/member.

Endpoint

GET https://scan.beartariacampgrounds.com/api/public/donor-eligibility?key=<SECRET>&email=<email>
  • key = the value of PUBLIC_LOOKUP_SECRET (set in the backend .env).
  • Returns minimal JSON — never names or dollar amounts:
    • {"eligible": true, "tier": "member"}
    • {"eligible": true, "tier": "donor"}
    • {"eligible": false, "tier": null}
  • Rate-limited (30/min/IP) and CORS-restricted to PUBLIC_LOOKUP_ORIGIN (default https://tickets.beartariacampgrounds.com).

The secret is visible in page source, so treat this as deterrence, not security. It only gates a discount and reveals a yes/no + tier, so the blast radius is small. Rotate the secret by changing PUBLIC_LOOKUP_SECRET and redeploying.

Form setup (conditional pricing)

The idea: a hidden field donor_tier holds regular / donor / member. The JS sets it from the email lookup, and your payment options are shown/hidden by FluentForms conditional logic based on its value.

  1. Hidden field. Add a Hidden Field, name it exactly donor_tier, default value regular.
  2. Email field. Note its name (default email).
  3. Payment options. Set up two payment items (or two options of a multiple-choice payment field) — a regular price and a discounted price — and give each conditional logic:
    • Regular price: show when donor_tier is regular
    • Donor price: show when donor_tier is donor OR donor_tier is member (add both rules with "match any").
  4. Custom HTML. Add a Custom HTML element and paste the snippet below, setting KEY to your PUBLIC_LOOKUP_SECRET (and EMAIL_SELECTOR if your email field isn't named email).

FluentForms is Vue-driven, so a plain input.value = … won't update its model and conditional logic won't fire. The snippet uses the native value setter + dispatches input/change, which is the reliable way to make FF notice a programmatic change. Test on your form; if conditional logic still doesn't react, tell me your FF version and I'll adapt.

Snippet

<div id="donor-status" style="margin:6px 0;font-size:14px;font-weight:600;"></div>
<script>
(function () {
  var API = "https://scan.beartariacampgrounds.com/api/public/donor-eligibility";
  var KEY = "REPLACE_WITH_PUBLIC_LOOKUP_SECRET";
  var EMAIL_SELECTOR = 'input[name="email"]';        // adjust if needed
  var TIER_SELECTOR  = 'input[name="donor_tier"]';   // the hidden field
  var statusEl = document.getElementById("donor-status");
  var lastChecked = "";

  // Set a framework-bound input's value so Vue/React actually notice it.
  function setNativeValue(el, value) {
    var proto = Object.getPrototypeOf(el);
    var protoSetter = Object.getOwnPropertyDescriptor(proto, "value");
    var ownSetter = Object.getOwnPropertyDescriptor(el, "value");
    if (ownSetter && protoSetter && ownSetter.set !== protoSetter.set) {
      protoSetter.set.call(el, value);
    } else if (protoSetter) {
      protoSetter.set.call(el, value);
    } else {
      el.value = value;
    }
    el.dispatchEvent(new Event("input", { bubbles: true }));
    el.dispatchEvent(new Event("change", { bubbles: true }));
  }

  function setTier(tier) {
    var el = document.querySelector(TIER_SELECTOR);
    if (el) setNativeValue(el, tier);   // "regular" | "donor" | "member"
  }

  function check(email) {
    if (!email || email === lastChecked) return;
    lastChecked = email;
    statusEl.textContent = "Checking donor status…";
    statusEl.style.color = "#888";
    fetch(API + "?key=" + encodeURIComponent(KEY) + "&email=" + encodeURIComponent(email))
      .then(function (r) { return r.json(); })
      .then(function (d) {
        if (d && d.eligible) {
          setTier(d.tier);                                // "member" or "donor"
          statusEl.textContent = (d.tier === "member" ? "🐻 Member" : "⭐ Donor") + " pricing unlocked!";
          statusEl.style.color = "#1b7f3b";
        } else {
          setTier("regular");
          statusEl.textContent = "";
        }
      })
      .catch(function () { setTier("regular"); statusEl.textContent = ""; });
  }

  function bind() {
    var el = document.querySelector(EMAIL_SELECTOR);
    if (!el) { return setTimeout(bind, 500); }   // form may render late
    setTier("regular");                          // start at regular price
    el.addEventListener("blur", function () { check(el.value.trim().toLowerCase()); });
  }
  bind();
})();
</script>

Test

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 price to show when donor_tier is not regular, and you can ignore the member/donor distinction.


Ticket-voucher entitlement

The donor ticket-voucher lookup (how many free tickets a donor earned) is a separate endpoint documented on its own page: fluentforms-ticket-vouchers.md.