Payment and missed-payment notices also land in the on-site inbox (login pop-up + Messages card), with who bought, share in POL and dollars, and the transaction link

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
martbost
2026-09-15 10:44:48 -05:00
parent d6d771d93e
commit 56aa7418ad
2 changed files with 40 additions and 21 deletions
+39 -20
View File
@@ -599,12 +599,32 @@ async function sendWelcome(email, ref) {
// on-chain event emails: a payout received, or a payout that passed you by
const weiToPol = w => { try { return (Number(BigInt(w) / (10n ** 14n)) / 10000).toString(); } catch (e) { return '?'; } };
async function emailOnEvent(ev) {
if (!mailer.hasKey() || !ev) return;
if (!ev) return;
const notify = async (memberId, subject, body) => {
if (!memberId) return;
if (!memberId || !mailer.hasKey()) return;
const a = await accounts.byMemberId(memberId);
if (a && a.email) mailer.send(a.email, subject, body + '\n\nSee it on the live ledger: https://instantadpay.com/ledger\n\nInstantAdPay').catch(() => {});
};
// Marty (2026-09-15): payment and missed-payment notices also land in the on-site inbox, so
// they are waiting (login modal + Messages card) whether or not the email was read. Sent as
// the company account (#1), the same sender the credit-return notes used. Plain text in, HTML out.
const inboxNote = async (memberId, subject, text) => {
if (!memberId) return;
try {
const a = await accounts.byMemberId(memberId);
if (!a || !a.email) return;
const html = '<p>' + String(text).replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;')
.replace(/(https:\/\/[^\s]+)/g, '<a href="$1" target="_blank" rel="noopener">$1</a>').split('\n\n').join('</p><p>').replace(/\n/g, '<br>') + '</p>';
await messages.deliver(1, ADMIN_EMAIL || 'house@instantadpay.com', [a.email], subject, html);
} catch (e) {}
};
const tell = async (memberId, subject, text) => { await notify(memberId, subject, text); await inboxNote(memberId, subject, text); };
const PCT = { 1: 50, 2: 20, 3: 10 };
// the Purchase event of the same tx is already indexed (it precedes every payout log), so the
// dollar side of any share is that purchase's price times the tier percentage
const purchaseOf = tx => { try { return chain.recentEvents(600).find(e => e.tx === tx && e.type === 'Purchase'); } catch (e) { return null; } };
const usdShare = (pur, pct) => pur ? (' (about ' + ('$' + (pur.priceCents * pct / 10000).toFixed(2)).replace(/\.00$/, '') + ')') : '';
const txUrlOf = tx => { const cc = chain.getConfig(); return (cc.explorer ? cc.explorer.replace(/\/+$/, '') : 'https://polygonscan.com') + '/tx/' + tx; };
if (ev.type === 'Purchase') {
const cc = chain.getConfig();
const txUrl = (cc.explorer ? cc.explorer.replace(/\/+$/, '') : 'https://polygonscan.com') + '/tx/' + ev.tx;
@@ -635,36 +655,35 @@ async function emailOnEvent(ev) {
}
} catch (e) {}
}
else if (ev.type === 'TierPaid') await notify(ev.recipientId, 'You just got paid on InstantAdPay', 'A level-' + ev.tier + ' payout of ' + weiToPol(ev.amountWei) + ' POL just landed in your wallet.');
else if (ev.type === 'AwardPaid') await notify(ev.toId, 'You just got paid on InstantAdPay', weiToPol(ev.amountWei) + ' POL just landed in your wallet.');
else if (ev.type === 'TierPaid') {
const pct = PCT[ev.tier] || 0; const pur = purchaseOf(ev.tx);
let buyer = 'a member on your level ' + ev.tier; try { buyer = await memberLabel(ev.buyerId); } catch (e) {}
const pol = weiToPol(ev.amountWei);
await tell(ev.recipientId, 'You just got paid ' + pol + ' POL on InstantAdPay',
buyer + ' just bought an ad package on your level ' + ev.tier + (pur ? ' ($' + (pur.priceCents / 100).toFixed(2).replace(/\.00$/, '') + ')' : '') + '. Your ' + pct + ' percent share, ' + pol + ' POL' + usdShare(pur, pct) + ', landed in your wallet in the same transaction' + (ev.hops ? ', passed up to you because someone between you was not qualified for this level' : '') + '.\n\n' +
'Transaction: ' + txUrlOf(ev.tx) + '\n\nYour dashboard: https://instantadpay.com/my');
}
else if (ev.type === 'AwardPaid') await tell(ev.toId, 'You just got paid ' + weiToPol(ev.amountWei) + ' POL on InstantAdPay', weiToPol(ev.amountWei) + ' POL just landed in your wallet.\n\nTransaction: ' + txUrlOf(ev.tx) + '\n\nYour dashboard: https://instantadpay.com/my');
else if (ev.type === 'PassedUp' && ev.reason === 'send-failed') {
// the member WAS qualified but their wallet rejected the POL (usually a smart-contract
// wallet that needs more than the capped gas): tell them and the admin, loudly
await notify(ev.skippedId, 'Your wallet rejected a payout on InstantAdPay', 'A level-' + ev.tier + ' payout tried to reach your linked wallet and the wallet refused the transfer, so it passed to the next qualified member. This happens with some smart-contract wallets. Link a regular wallet address (MetaMask, SafePal, Phantom) on the Wallet tab so the next payout lands.');
await tell(ev.skippedId, 'Your wallet rejected a payout on InstantAdPay', 'A level-' + ev.tier + ' payout tried to reach your linked wallet and the wallet refused the transfer, so it passed to the next qualified member. This happens with some smart-contract wallets. Link a regular wallet address (MetaMask, SafePal, Phantom) on the Wallet tab so the next payout lands.\n\nTransaction: ' + txUrlOf(ev.tx));
if (ADMIN_EMAIL) mailer.send(ADMIN_EMAIL, 'InstantAdPay: payout send-failed for member #' + ev.skippedId, 'A level-' + ev.tier + ' payout to member #' + ev.skippedId + ' failed at the wallet (send-failed) and passed up. Tx: ' + ev.tx + '\n\nThe member has been emailed to link a regular wallet.').catch(() => {});
}
else if (ev.type === 'PassedUp') {
// Marty (2026-09-15): a member who missed a payout because they were not qualified must be told
// exactly what they missed. The Purchase event of the same tx is already indexed (it precedes
// the PassedUp log), so the amount is that purchase's share for the tier: 50 / 20 / 10 percent.
const PCT = { 1: 50, 2: 20, 3: 10 }; const pct = PCT[ev.tier] || 0;
let pol = '', usd = '', buyer = 'a member on your level ' + ev.tier;
try {
const pur = chain.recentEvents(600).find(e => e.tx === ev.tx && e.type === 'Purchase');
if (pur) {
pol = weiToPol((BigInt(pur.paidWei) * BigInt(pct) / 100n).toString());
usd = ('$' + (pur.priceCents * pct / 10000).toFixed(2)).replace(/\.00$/, '');
}
buyer = await memberLabel(ev.buyerId);
} catch (e) {}
// exactly what they missed: who bought, the share in POL and dollars, and how to close the gap.
const pct = PCT[ev.tier] || 0; const pur = purchaseOf(ev.tx);
let pol = '', buyer = 'a member on your level ' + ev.tier;
try { if (pur) pol = weiToPol((BigInt(pur.paidWei) * BigInt(pct) / 100n).toString()); buyer = await memberLabel(ev.buyerId); } catch (e) {}
let bc = 0; try { bc = (await chain.member(ev.skippedId)).buyerCount || 0; } catch (e) {}
const need = ev.tier === 3 ? 5 : 2, short = Math.max(0, need - bc);
const what = pol ? pol + ' POL (about ' + usd + ')' : 'a level-' + ev.tier + ' payout';
await notify(ev.skippedId, 'You missed ' + (pol ? pol + ' POL' : 'a payout') + ' on InstantAdPay',
const what = pol ? pol + ' POL' + usdShare(pur, pct) : 'a level-' + ev.tier + ' payout';
await tell(ev.skippedId, 'You missed ' + (pol ? pol + ' POL' : 'a payout') + ' on InstantAdPay',
buyer + ' just bought an ad package on your level ' + ev.tier + '. Your share was ' + what + ', and it passed you by because level ' + ev.tier + ' is not open on your account yet. The contract paid it to the next qualified person above you.\n\n' +
'Level ' + ev.tier + ' opens at ' + need + ' qualifying buyers (people you referred who bought a $20 or larger package). You have ' + bc + (short ? ', so you are ' + short + ' buyer' + (short === 1 ? '' : 's') + ' away.' : '.') + '\n\n' +
'Two ways to close the gap: bring ' + (short || 1) + ' more buyer' + (short === 1 ? '' : 's') + ' from your My line page, or use Qualified Start under Buy packages to be your own buyer today. Every package on level ' + ev.tier + ' pays you ' + pct + ' percent once it is open, and the next one is coming whether you are ready or not.\n\n' +
'Your dashboard: https://instantadpay.com/my');
'Transaction: ' + txUrlOf(ev.tx) + '\n\nYour dashboard: https://instantadpay.com/my');
}
}
// payment-proof Telegram feed (same pattern as the RM Circle proof channel): one compact