diff --git a/server.js b/server.js index a95f441..ca4f58c 100644 --- a/server.js +++ b/server.js @@ -619,6 +619,8 @@ async function sponsorSyncOnEvent(ev) { const ref = spAcct && spAcct.code ? spAcct.code : String(onchain); const old = acct.sponsorRef || ''; await accounts.setSponsorRef(acct.email, ref); + movedByTx.set(ev.tx, { buyerId: Number(ev.id) || 0, toId: onchain, oldRef: old, at: Date.now() }); // read by telegramOnEvent on the Purchase in the same tx + if (movedByTx.size > 200) { const k = movedByTx.keys().next().value; movedByTx.delete(k); } console.log('referral moved to where the pay landed:', acct.email, JSON.stringify(old), '->', ref, '(#' + onchain + ')'); // Notices, now that it is a fact on the chain (Marty, 2026-09-16): everyone between the stored // referrer and the paid sponsor who was simply not activated is told they lost this referral for @@ -638,6 +640,7 @@ async function sponsorSyncOnEvent(ev) { if (spAcct && lost.length) sponsorGainNudge(spAcct, nameOf(acct), lost.map(nameOf)).catch(() => {}); } catch (e) { console.error('sponsor move notices', e.message); } } +const movedByTx = new Map(); // tx -> { buyerId, toId, oldRef } for purchases routed past an unactivated sponsor async function refAccount(ref) { const t = String(ref || '').trim().toLowerCase(); if (!t) return null; let a = await accounts.byCode(t); if (!a) a = await accounts.byUsername(t); @@ -883,8 +886,41 @@ async function telegramOnEvent(ev) { if (!line) return null; return line + ' \u00b7 verify' + (sc.telegramCtaUrl ? '\nJoin free' : ''); }; - if (sc.telegramChatId) { const t = build(String(sc.telegramEvents || 'payouts')); if (t) await telegramSend(sc.telegramChatId, t, sc.telegramTopicId); } + // Missed-payout lines (Marty, 2026-09-16): when a share is passed over, say who missed it and who got it. + // (a) on-chain pass-up: the TierPaid with hops>0 follows the PassedUp events of the same tx, already indexed. + // (b) off-chain walk-up past an unactivated sponsor: recorded by sponsorSyncOnEvent, posted on the Purchase. + let missed = null; + try { + const site = 'https://instantadpay.com/my'; + if (ev.type === 'TierPaid' && Number(ev.hops) > 0) { + const all = chain.recentEvents(1e9); + const sk = all.filter(x => x.type === 'PassedUp' && x.tx === ev.tx && x.tier === ev.tier); + if (sk.length) { + const nm2 = await accounts.namesForMembers(sk.map(x => x.skippedId)).catch(() => ({})); + const need = ev.tier === 2 ? 2 : 5; + const parts = sk.map(x => { + const had = all.filter(y => y.type === 'BuyerCounted' && y.sponsorId === x.skippedId && y.block < ev.block).length; + const why = x.reason === 'unqualified' ? 'not qualified for level ' + ev.tier + ' yet: ' + had + ' of ' + need + ' qualifying buyers' : x.reason === 'send-failed' ? 'the wallet refused the payment' : String(x.reason || ''); + return '#' + x.skippedId + (nm2[x.skippedId] ? ' @' + nm2[x.skippedId] : '') + ' (' + why + ')'; + }); + missed = '\u{1F62C} Missed payout: ' + parts.join(' and ') + ' passed over on ' + who(ev.buyerId) + "'s purchase. " + weiToPol(ev.amountWei) + ' POL went to ' + who(ev.recipientId) + ' instead.' + + '\nQualify before the next one: ' + site.replace(/^https:\/\//, '') + ''; + } + } + if (ev.type === 'Purchase' && movedByTx.has(ev.tx)) { + const mv = movedByTx.get(ev.tx); movedByTx.delete(ev.tx); + const lostAcct = await refAccount(mv.oldRef); + if (lostAcct) { + const lostName = lostAcct.username ? '@' + lostAcct.username : lostAcct.email.replace(/@.*/, '') + '@'; + const nm3 = await accounts.namesForMembers([mv.toId]).catch(() => ({})); + missed = '\u{1F62C} Missed sale: ' + lostName + ' had not switched on payouts, so ' + who(ev.buyerId) + "'s $" + Math.round(ev.priceCents / 100) + ' purchase routed past them. That buyer, and every payout from them, now belongs to #' + mv.toId + (nm3[mv.toId] ? ' @' + nm3[mv.toId] : '') + ' for good.' + + '\nTwo minutes on the Wallet tab prevents this: ' + site.replace(/^https:\/\//, '') + ''; + } + } + } catch (e) { console.error('missed-payout post', e.message); } + if (sc.telegramChatId) { const t = build(String(sc.telegramEvents || 'payouts')); if (t) await telegramSend(sc.telegramChatId, t, sc.telegramTopicId); if (missed && String(sc.telegramEvents || 'payouts') !== 'none') await telegramSend(sc.telegramChatId, missed, sc.telegramTopicId).catch(() => {}); } if (sc.telegramEchoChatId) { const t = build(String(sc.telegramEchoEvents || 'payouts')); if (t) await telegramSend(sc.telegramEchoChatId, '\u{1F7E0} InstantAdPay \u00b7 ' + t, sc.telegramEchoTopicId); } + if (missed && sc.telegramEchoChatId && String(sc.telegramEchoEvents || 'payouts') !== 'none') await telegramSend(sc.telegramEchoChatId, '\u{1F7E0} InstantAdPay \u00b7 ' + missed, sc.telegramEchoTopicId).catch(() => {}); } // holding-tank arrivals -> one digest line in the shared payments topic (Marty, 2026-09-12): who is // waiting for a sponsor, by username, so builders go adopt them. Runs every 15 min, posts only