Sponsor hold: a named sponsor that cannot be paid (payouts off, chain lookup failed, unknown) blocks the first purchase and activation with a message and alerts the admin, instead of silently crediting the company
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
@@ -514,14 +514,28 @@ async function payoutChainBlocked(id, hops) {
|
||||
for (let i = 0; i <= hops && cur; i++) { if (bad.has(cur)) return cur; let m = null; try { m = await chain.member(cur); } catch (e) { break; } cur = m ? Number(m.sponsorId) || 0 : 0; }
|
||||
return 0;
|
||||
}
|
||||
async function resolveSponsorToken(tok) {
|
||||
// Why a sponsor token did not resolve matters (vladz79 → #24 locked under the company on 2026-09-13 because a
|
||||
// lookup came back empty and the code fell back silently): 'ok' | 'none' (no token) | 'unknown' (no such account)
|
||||
// | 'notActivated' (sponsor has no wallet / payouts off) | 'rpc' (chain lookup failed right now).
|
||||
async function resolveSponsorDetailed(tok) {
|
||||
const t = String(tok || '').trim().toLowerCase();
|
||||
if (!t) return 0;
|
||||
if (/^\d+$/.test(t)) return Number(t);
|
||||
if (!t) return { id: 0, reason: 'none' };
|
||||
if (/^\d+$/.test(t)) return { id: Number(t), reason: 'ok' };
|
||||
let acct = await accounts.byCode(t);
|
||||
if (!acct) acct = await accounts.byUsername(t); // vanity links: /join/<username>
|
||||
if (!acct || !acct.address) return 0;
|
||||
try { return await chain.memberIdByAccount(acct.address); } catch (e) { return 0; }
|
||||
if (!acct) return { id: 0, reason: 'unknown', name: t };
|
||||
if (!acct.address) return { id: 0, reason: 'notActivated', name: acct.username ? '@' + acct.username : t };
|
||||
try { const id = await chain.memberIdByAccount(acct.address); return { id, reason: id ? 'ok' : 'notActivated', name: acct.username ? '@' + acct.username : t }; }
|
||||
catch (e) { return { id: 0, reason: 'rpc', name: acct.username ? '@' + acct.username : t }; }
|
||||
}
|
||||
async function resolveSponsorToken(tok) { return (await resolveSponsorDetailed(tok)).id; }
|
||||
const sponsorAlertLast = new Map(); // email -> ts (one alert per member per hour)
|
||||
function sponsorBlockedAlert(who, r) {
|
||||
const k = String(who || '?'); if (Date.now() - (sponsorAlertLast.get(k) || 0) < 3600000) return; sponsorAlertLast.set(k, Date.now());
|
||||
const text = '\u26A0\uFE0F InstantAdPay: purchase held for ' + k.replace(/^(.{2}).*(@.*)$/, '$1***$2') + '. Their sponsor ' + (r.name || r.tok || '?') + ' could not be resolved (' + r.reason + '), so the buy was blocked instead of crediting the company. ' + (r.reason === 'notActivated' ? 'The sponsor needs to switch on payouts.' : r.reason === 'rpc' ? 'Chain lookup failed; they can retry.' : 'Check the sponsor field in Admin > Members.');
|
||||
const sc = siteConfig();
|
||||
if (sc.telegramBotToken && sc.telegramAdminChatId) telegramSend(sc.telegramAdminChatId, text).catch(() => {});
|
||||
else if (ADMIN_EMAIL && mailer.hasKey()) mailer.send(ADMIN_EMAIL, 'InstantAdPay: purchase held, sponsor unresolved', text).catch(() => {});
|
||||
}
|
||||
// The moment someone joins through a code, nudge its owner to activate.
|
||||
// Email a member's sponsor the moment they get a new referral (free OR paid).
|
||||
@@ -847,7 +861,9 @@ const server = http.createServer(async (req, res) => {
|
||||
const s = await auth.fromRequest(req);
|
||||
const acct = s && s.email ? await accounts.byEmail(s.email) : null;
|
||||
const tok = (acct && acct.sponsorRef) || parseCookies(req)['iap.sponsor'] || '';
|
||||
let sponsorId = await resolveSponsorToken(tok);
|
||||
const spd = await resolveSponsorDetailed(tok); let sponsorId = spd.id;
|
||||
let sponsorBlocked = null; // set when the account itself names a sponsor that cannot be paid right now: the client refuses the transaction
|
||||
if (acct && acct.sponsorRef && !sponsorId && spd.reason !== 'none') { sponsorBlocked = spd.reason; sponsorBlockedAlert(acct.email, Object.assign({ tok }, spd)); }
|
||||
if (sponsorId && await payoutChainBlocked(sponsorId, 2)) { console.log('sponsor routed away from no-payout chain', sponsorId); sponsorId = 0; }
|
||||
// orphan fallback: an unresolvable/absent sponsor (dead link, no link) lands
|
||||
// the new member under the configured catch position (#1) instead of root
|
||||
@@ -864,7 +880,7 @@ const server = http.createServer(async (req, res) => {
|
||||
if (!a && /^\d+$/.test(nameTok)) a = await accounts.byMemberId(Number(nameTok));
|
||||
if (a) { name = a.username ? '@' + a.username : (a.memberId ? 'member #' + a.memberId : null); avatarUrl = a.avatarUrl || null; own = !!(acct && a.email === acct.email); var bio = null, cobrand = false; try { cobrand = (await ads.milestonesOf(a.email)).includes('level3'); if (cobrand) bio = a.bio ? String(a.bio).slice(0, 220) : null; } catch (e) {} }
|
||||
}
|
||||
return json(res, 200, { ref: tok, sponsorId, invited: !!(tok || showTok), name, avatarUrl, own, bio: typeof bio === 'undefined' ? null : bio, cobrand: typeof cobrand === 'undefined' ? false : cobrand });
|
||||
return json(res, 200, { ref: tok, sponsorId, sponsorBlocked, sponsorName: spd.name || null, invited: !!(tok || showTok), name, avatarUrl, own, bio: typeof bio === 'undefined' ? null : bio, cobrand: typeof cobrand === 'undefined' ? false : cobrand });
|
||||
}
|
||||
if (p === '/api/stats' && req.method === 'GET') {
|
||||
let members = 0; try { members = await chain.memberCount(); } catch (e) {}
|
||||
@@ -1032,12 +1048,13 @@ const server = http.createServer(async (req, res) => {
|
||||
if (!s) return json(res, 200, { signedIn: false });
|
||||
const memberId = await auth.refreshMemberId(s);
|
||||
const acct = (s.email && await accounts.byEmail(s.email)) || (s.address && await accounts.byAddress(s.address)) || null;
|
||||
let sponsorId = await resolveSponsorToken((acct && acct.sponsorRef) || parseCookies(req)['iap.sponsor']);
|
||||
const spdMe = await resolveSponsorDetailed((acct && acct.sponsorRef) || parseCookies(req)['iap.sponsor']); let sponsorId = spdMe.id;
|
||||
const sponsorBlocked = (acct && acct.sponsorRef && !sponsorId && spdMe.reason !== 'none') ? spdMe.reason : null;
|
||||
if (sponsorId && await payoutChainBlocked(sponsorId, 2)) sponsorId = 0;
|
||||
const _defSpon = Number(siteConfig().defaultSponsorId) || 1;
|
||||
if (!sponsorId && memberId !== _defSpon) sponsorId = _defSpon; // orphan fallback → #1 (never self-sponsor)
|
||||
if (memberId && acct && acct.memberId !== memberId) accounts.setMemberId(acct.email, memberId).catch(() => {});
|
||||
const out = { signedIn: true, email: s.email || (acct && acct.email) || null,
|
||||
const out = { signedIn: true, sponsorBlocked, sponsorName: spdMe.name || null, email: s.email || (acct && acct.email) || null,
|
||||
address: s.address || (acct && acct.address) || null, memberId,
|
||||
username: (acct && acct.username) || null,
|
||||
refCode: (acct && acct.code) || null, sponsorId,
|
||||
|
||||
Reference in New Issue
Block a user