Missed-payout notice shows the whole chain, one message per purchase

Marty (2026-09-19): show every missed qualification so the whole chain is visible. The old
notice fired once per passed-up tier and showed only that tier. AdminPaid is the last event of
every purchase transaction (119 of 119 in the index), so the notice now fires there, gathers
every PassedUp and TierPaid of the tx, and missed.js composes one message: each tier's walk,
every person passed over with their buyer count, who finally caught it (or that the company
kept it), and the total POL that walked past how many people on that one sale.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
martbost
2026-09-19 13:14:12 -05:00
parent d90f08d54a
commit c00ec64cf8
2 changed files with 65 additions and 13 deletions
+53
View File
@@ -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} <b>Missed payouts</b> 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 + ' · <b>' + pol.toFixed(4) + ' POL</b>: ' + hops.join(' → ') + ' → ' + end);
}
const n = passed.size;
lines.push('<b>' + walked.toFixed(4) + ' POL walked past ' + n + ' ' + (n === 1 ? 'person' : 'people') + ' on one ' + price + ' sale.</b>');
lines.push('Qualify before the next one: <a href="' + site + '">' + site.replace(/^https:\/\//, '') + '</a>');
return lines.join('\n');
}
module.exports = { composeMissed, fmtPol };
+12 -13
View File
@@ -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 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 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 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 blog = require('./blog');
const adminMember = require('./adminmember'); const adminMember = require('./adminmember');
const syndicate = require('./syndicate'); const syndicate = require('./syndicate');
@@ -952,19 +953,17 @@ async function telegramOnEvent(ev) {
let missed = null; let missed = null;
try { try {
const site = 'https://instantadpay.com/my'; 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 all = chain.recentEvents(1e9);
const sk = all.filter(x => x.type === 'PassedUp' && x.tx === ev.tx && x.tier === ev.tier); const txEvents = all.filter(x => x.tx === ev.tx);
if (sk.length) { if (txEvents.some(x => x.type === 'PassedUp')) {
const nm2 = await accounts.namesForMembers(sk.map(x => x.skippedId)).catch(() => ({})); const ids = new Set(); txEvents.forEach(x => { for (const k of ['buyerId', 'skippedId', 'recipientId']) if (x[k]) ids.add(x[k]); });
const need = ev.tier === 2 ? 2 : 5; const nm2 = await accounts.namesForMembers([...ids]).catch(() => ({}));
const parts = sk.map(x => { const who2 = id => '#' + id + (nm2[id] ? ' @' + nm2[id] : '');
const had = all.filter(y => y.type === 'BuyerCounted' && y.sponsorId === x.skippedId && y.block < ev.block).length; const had = (id, tier) => all.filter(y => y.type === 'BuyerCounted' && y.sponsorId === id && 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 || ''); missed = missedNotice.composeMissed(txEvents, who2, had, site);
return '#' + x.skippedId + (nm2[x.skippedId] ? ' @' + nm2[x.skippedId] : '') + ' (' + why + ')';
});
missed = '\u{1F62C} <b>Missed payout</b>: ' + parts.join(' and ') + ' passed over on ' + who(ev.buyerId) + "'s purchase. <b>" + weiToPol(ev.amountWei) + ' POL</b> went to ' + who(ev.recipientId) + ' instead.'
+ '\nQualify before the next one: <a href="' + site + '">' + site.replace(/^https:\/\//, '') + '</a>';
} }
} }
if (ev.type === 'Purchase' && movedByTx.has(ev.tx)) { if (ev.type === 'Purchase' && movedByTx.has(ev.tx)) {