diff --git a/missed.js b/missed.js new file mode 100644 index 0000000..f2c1eda --- /dev/null +++ b/missed.js @@ -0,0 +1,53 @@ +// The missed-payout notice, whole chain in one message (Marty, 2026-09-19). +// +// A purchase pays three tiers up the sponsor line. When a tier's rightful recipient is not +// qualified, the contract passes that share up until it finds someone who is, or hands it to +// the company. Every hop is an on-chain event: PassedUp (who was skipped, and why) and TierPaid +// (who finally got it, and how many hops up). Until now the Telegram notice reported one tier +// per message; this composes ONE message per purchase that shows every tier's walk, every +// person passed, and what it added up to. The point is that the people passed over see, in +// numbers, exactly what qualifying would have paid them. +// +// Pure: takes the events of one transaction plus a name resolver; returns the text or null. +'use strict'; + +const SHARE = { 1: 0.5, 2: 0.2, 3: 0.1 }; // tier shares of the purchase, for a tier nobody caught +const NEED = { 2: 2, 3: 5 }; // qualifying buyers a tier requires + +function fmtPol(wei) { + try { return (Number(BigInt(String(wei)) / 10n ** 14n) / 1e4).toFixed(4); } catch (e) { return '0'; } +} + +// txEvents: every indexed event sharing the tx. who(id) -> '#id @name'. had(id, tier) -> qualifying +// buyers that member had at the time (a number) or null when unknown. +function composeMissed(txEvents, who, had, site) { + const buy = txEvents.find(e => e.type === 'Purchase'); + const skips = txEvents.filter(e => e.type === 'PassedUp'); + if (!buy || !skips.length) return null; + const price = '$' + Math.round((buy.priceCents || 0) / 100); + const lines = ['\u{1F62C} Missed payouts on ' + who(buy.buyerId) + "'s " + price + ' purchase:']; + const passed = new Set(); let walked = 0; + for (const tier of [2, 3]) { + const sk = skips.filter(s => Number(s.tier) === tier); + if (!sk.length) continue; + const paid = txEvents.find(e => e.type === 'TierPaid' && Number(e.tier) === tier); + const pol = paid ? Number(fmtPol(paid.amountWei)) : Number(fmtPol(BigInt(String(buy.paidWei || 0)) * BigInt(Math.round(SHARE[tier] * 100)) / 100n)); + walked += pol; + const hops = sk.map(s => { + passed.add(s.skippedId); + let why; + if (s.reason === 'unqualified') { const n = had(s.skippedId, tier); why = (n == null ? 'not qualified' : n + ' of ' + NEED[tier] + ' qualifying buyers'); } + else if (s.reason === 'send-failed') why = 'wallet refused the payment'; + else why = String(s.reason || 'passed'); + return who(s.skippedId) + ' (' + why + ')'; + }); + const end = paid ? 'paid to ' + who(paid.recipientId) : 'nobody above was qualified, so the company kept it'; + lines.push('Level ' + tier + ' · ' + pol.toFixed(4) + ' POL: ' + hops.join(' → ') + ' → ' + end); + } + const n = passed.size; + lines.push('' + walked.toFixed(4) + ' POL walked past ' + n + ' ' + (n === 1 ? 'person' : 'people') + ' on one ' + price + ' sale.'); + lines.push('Qualify before the next one: ' + site.replace(/^https:\/\//, '') + ''); + return lines.join('\n'); +} + +module.exports = { composeMissed, fmtPol }; diff --git a/server.js b/server.js index 2024c24..56ccbd3 100644 --- a/server.js +++ b/server.js @@ -28,7 +28,8 @@ const coach = require('./coach'); // coaching view, nudges, digest, prospects, const tank = require('./tank'); // holding tank: unsponsored free members, adoptions, pay-it-forward const legacy = require('./legacy'); // Faucet Wave / Tier One Ads bridge: welcome-back credits for listed emails const traffic = require('./traffic'); // public page views by referring domain (admin Traffic tab) -const promos = require('./promos'); // partner promo codes -> free ad credits (link ?promo=CODE or the dashboard box) +const promos = require('./promos'); +const missedNotice = require('./missed'); // the whole-chain missed-payout notice // partner promo codes -> free ad credits (link ?promo=CODE or the dashboard box) const blog = require('./blog'); const adminMember = require('./adminmember'); const syndicate = require('./syndicate'); @@ -952,19 +953,17 @@ async function telegramOnEvent(ev) { let missed = null; try { const site = 'https://instantadpay.com/my'; - if (ev.type === 'TierPaid' && Number(ev.hops) > 0) { + // (a) on-chain pass-ups: AdminPaid is the last event of every purchase tx, so by then every + // PassedUp and TierPaid of the tx is indexed; one message shows the whole chain (missed.js). + if (ev.type === 'AdminPaid') { 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:\/\//, '') + ''; + const txEvents = all.filter(x => x.tx === ev.tx); + if (txEvents.some(x => x.type === 'PassedUp')) { + const ids = new Set(); txEvents.forEach(x => { for (const k of ['buyerId', 'skippedId', 'recipientId']) if (x[k]) ids.add(x[k]); }); + const nm2 = await accounts.namesForMembers([...ids]).catch(() => ({})); + const who2 = id => '#' + id + (nm2[id] ? ' @' + nm2[id] : ''); + const had = (id, tier) => all.filter(y => y.type === 'BuyerCounted' && y.sponsorId === id && y.block < ev.block).length; + missed = missedNotice.composeMissed(txEvents, who2, had, site); } } if (ev.type === 'Purchase' && movedByTx.has(ev.tx)) {