Sponsor notifications: new referral + referral purchase

- notifyNewReferral (was nudgeReferrer): email a member's sponsor on EVERY new
  referral, activated or not. Resolve the sponsor from the join token by member
  id, share code, or username (was code-only and skipped already-activated
  sponsors). Reworded from an activation nudge to a real "you have a new
  referral" note; keeps the payouts reminder only for un-activated sponsors.
- Purchase event now also emails the buyer's direct sponsor that their referral
  bought a package, noting whether it's a $20+ qualifying purchase.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
martbost
2026-09-08 11:34:34 -05:00
parent 873a9e5e4a
commit f53bb7db85
+42 -12
View File
@@ -238,18 +238,29 @@ async function resolveSponsorToken(tok) {
try { return await chain.memberIdByAccount(acct.address); } catch (e) { return 0; }
}
// The moment someone joins through a code, nudge its owner to activate.
async function nudgeReferrer(ref) {
// Email a member's sponsor the moment they get a new referral (free OR paid).
// Resolves the sponsor from the join token by member id, share code, or username,
// and notifies EVERY sponsor — activated or not (an active sponsor still wants to
// know their team grew). A referral is on the line from signup; it only counts
// toward qualification once it makes a $20+ purchase.
async function notifyNewReferral(ref, newAcct) {
try {
if (!mailer.hasKey()) return;
const t = String(ref || '').trim().toLowerCase();
if (!t || /^\d+$/.test(t) || !mailer.hasKey()) return;
const owner = await accounts.byCode(t);
if (!owner || !owner.email || owner.address) return; // already activated-ready
mailer.send(owner.email, 'Someone just joined through your InstantAdPay link',
'Good news: a new member just signed up through your share link.\n\n'
+ 'One thing to do so you never miss a payment: sign in and switch on payouts '
+ '(one free wallet step). The contract locks each buyer to their sponsor at their '
+ 'first purchase, so have payouts on before your people start buying.\n\n'
+ 'https://instantadpay.com/my\n\nInstantAdPay').catch(e => console.error('nudge failed', e.message));
if (!t) return;
let owner = null;
if (/^\d+$/.test(t)) { try { owner = await accounts.byMemberId(Number(t)); } catch (e) {} }
if (!owner) { try { owner = await accounts.byCode(t); } catch (e) {} }
if (!owner) { try { owner = await accounts.byUsername(t); } catch (e) {} }
if (!owner || !owner.email) return;
const who = newAcct && newAcct.username ? '@' + newAcct.username : 'A new member';
let body = who + ' just joined InstantAdPay through your link — they are on your team from today.\n\n'
+ 'They count toward your qualification once they make a $20+ purchase.\n\n';
if (!owner.address) body += 'Make sure payouts are switched on (one free wallet step) so you never miss a commission — '
+ 'the contract locks each buyer to their sponsor at their first purchase.\n\n';
body += 'See your team: https://instantadpay.com/my\n\nInstantAdPay';
mailer.send(owner.email, 'You have a new referral on InstantAdPay', body)
.catch(e => console.error('referral notify failed', e.message));
} catch (e) {}
}
// welcome email on a new account: onboarding steps + who their sponsor is
@@ -288,6 +299,25 @@ async function emailOnEvent(ev) {
'Your ad-credit balance is now: ' + bal + '\n' +
'Amount paid: ' + weiToPol(ev.paidWei) + ' POL\n\n' +
'View your transaction on the blockchain:\n' + txUrl);
// tell the buyer's DIRECT sponsor their referral just bought (upline earners
// are separately notified by the TierPaid payout email when they earn)
try {
const buyer = await chain.member(ev.buyerId);
if (buyer && buyer.sponsorId) {
const sp = await accounts.byMemberId(buyer.sponsorId);
if (sp && sp.email) {
const ba = await accounts.byMemberId(ev.buyerId);
const bn = ba && ba.username ? '@' + ba.username : 'One of your referrals';
const usd = ('$' + (ev.priceCents / 100).toFixed(2)).replace(/\.00$/, '');
const qual = ev.priceCents >= 2000
? ' This is a $20+ purchase, so it counts toward your qualification.'
: ' (Purchases under $20 do not count toward qualification.)';
mailer.send(sp.email, 'Your referral just bought an ad package',
bn + ' just purchased a package (' + usd + ' — ' + ev.creditAmount + ' credits).' + qual + '\n\n' +
'See your team and the live ledger: https://instantadpay.com/my\n\nInstantAdPay').catch(() => {});
}
}
} 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.');
@@ -420,7 +450,7 @@ const server = http.createServer(async (req, res) => {
const ref = parseCookies(req)['iap.sponsor'] || ''; // first-touch attribution
const r = await accounts.signup(b.email, b.password, ref);
if (r.error) return json(res, 400, r);
nudgeReferrer(ref).catch(() => {});
notifyNewReferral(ref, r.account).catch(() => {});
if (b.newsletter) sendy.subscribe(r.account.email, r.account.username || '').catch(() => {}); // pre-checked opt-in, silent
const token = await auth.mintSession({ email: r.account.email });
return json(res, 200, { ok: true, account: r.account }, { 'Set-Cookie': auth.sessionCookie(token) });
@@ -466,7 +496,7 @@ const server = http.createServer(async (req, res) => {
const ref = parseCookies(req)['iap.sponsor'] || '';
const r = await accounts.ensure(e, ref); // first touch wins; existing accounts unchanged
if (r.error) return json(res, 400, r);
if (r.created) { nudgeReferrer(ref).catch(() => {}); sendWelcome(e, ref).catch(() => {}); }
if (r.created) { notifyNewReferral(ref, r.account).catch(() => {}); sendWelcome(e, ref).catch(() => {}); }
if (r.created && b.newsletter) sendy.subscribe(r.account.email, r.account.username || '').catch(() => {}); // pre-checked opt-in, silent, new joins only
let memberId = 0;
if (r.account.address) { try { memberId = await chain.memberIdByAccount(r.account.address); } catch (err) {} }