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 {