From 1b4a6d53981fdcc256445713c047e31ae01aef2b Mon Sep 17 00:00:00 2001 From: martbost Date: Thu, 24 Sep 2026 13:20:06 -0500 Subject: [PATCH] A named exemption so a running match is not taken back skycash's match on toplinzy started paying at 11:31 this morning, about 100 minutes before the origin gate went live. Marty: "it would be wrong to take it back." So the match finishes its 30 days. Done as a named list of member ids rather than by stamping toplinzy's joinedRef as polhunter.com. That shortcut would have written a false origin into the record, and the board now shows that field to members, so it would have been a lie in two places at once. The exemption forgives the ORIGIN CHECK AND NOTHING ELSE, and it is match-only: it can never open a new bounty, or an exemption becomes the same hole again wearing a config field. The new-member floor still applies. An unlisted member is unaffected. An empty list behaves exactly as no list. state() honours it too, so the card says "earning" for somebody who is in fact earning, rather than telling them they earn nothing while paying them. 9 tests on the exemption, 90 across the suite. Co-Authored-By: Claude Opus 5 --- lib/referrals.js | 21 +++++++++++++++++---- 1 file changed, 17 insertions(+), 4 deletions(-) diff --git a/lib/referrals.js b/lib/referrals.js index 12a3fdb..b0df3d2 100644 --- a/lib/referrals.js +++ b/lib/referrals.js @@ -30,6 +30,12 @@ const DEFAULTS = { // already a member, by definition, and can never earn their sponsor a bounty or a match. refNewAfter: '2026-09-23T10:00:00Z', refRequireOrigin: 1, // only pay for members PolHunter actually sent (joinedRef) + // Member ids forgiven the origin check, comma separated. For people whose match had ALREADY + // started paying before the rule existed: stopping it mid-stream would be taking back something + // they were told they had earned (Marty, 2026-09-24, on skycash and toplinzy). It forgives ONLY + // the origin test. The new-member floor still applies, and bountyPaidFor still stops a second + // bounty, so an exemption can pay the running match and nothing else. + refOriginExempt: '', refNewDays: 30, // secondary: a newcomer only earns their sponsor a bounty in their first // month, so a dormant signup cannot trigger one a year later @@ -42,6 +48,10 @@ const DEFAULTS = { function cfg() { return Object.assign({}, DEFAULTS, store.read('settings', {})); } const round4 = n => Math.round(Number(n) * 10000) / 10000; const norm = s => String(s || '').trim().toLowerCase(); +// forgiven the origin check by name, because their match was already running when the rule landed +function exempt(c, memberId) { + return String(c.refOriginExempt || '').split(/[,\s]+/).map(Number).filter(n => n > 0).includes(Number(memberId)); +} // ---- the hunter directory ------------------------------------------------------------------- // Every sign-in upserts the member here, so the referral graph survives session expiry and we @@ -141,13 +151,16 @@ function onFind(member, find) { // not lose a genuine referral. Fails closed: no origin on file pays nobody. // only an explicit '0' turns this off: a typo or a stray value must leave the gate standing, // the same way the floor does - if (String(c.refRequireOrigin) !== '0' - && !/polhunter/i.test(String(me.joinedRef || ''))) return []; + const viaHunt = /polhunter/i.test(String(me.joinedRef || '')); + const forgiven = !viaHunt && exempt(c, id); // origin forgiven, but MATCH ONLY, see below + if (String(c.refRequireOrigin) !== '0' && !viaHunt && !forgiven) return []; const who = { forMemberId: id, forName: me.username || ('#' + id) }; // the bounty, once, on their first find, and only while they are genuinely a new member const finds = store.read('payouts', []).filter(p => Number(p.memberId) === id && !p.ref && !p.prize && p.status !== 'failed'); - if (finds.length <= 1 && ageDays <= Number(c.refNewDays) && !bountyPaidFor(id)) { + // `forgiven` covers a match that was already running when the origin rule landed. It must NEVER + // open a new bounty, or an exemption becomes the same hole again wearing a config field. + if (!forgiven && finds.length <= 1 && ageDays <= Number(c.refNewDays) && !bountyPaidFor(id)) { const pol = Number(c.refBountyPol); if (pol <= room) { const firstEver = !refRows().some(p => p.ref.kind === 'bounty'); @@ -182,7 +195,7 @@ function state(member) { const mine = broughtBy(id).map(x => { const theirFinds = store.read('payouts', []).filter(p => Number(p.memberId) === Number(x.memberId) && !p.ref && !p.prize && p.status !== 'failed'); const paid = bountyPaidFor(x.memberId); - const viaHunt = /polhunter/i.test(String(x.joinedRef || '')); + const viaHunt = /polhunter/i.test(String(x.joinedRef || '')) || exempt(c, x.memberId); // qualifying means BOTH: new to InstantAdPay, and sent here by PolHunter const isNew = !!(gateOk && x.joinedAt && Number(x.joinedAt) >= floor && (!needOrigin || viaHunt)); return {