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

152 lines
6 KiB
Markdown

# 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
```html
<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`](./fluentforms-ticket-vouchers.md).