Payment trace: self-serve "who was paid, who was skipped and why" per purchase (Earnings tab + admin member card)

Marty, 2026-09-16, after the third "why didn't my sponsor get paid" thread of the day (livedreams / gracie25,
Morten / Terry's linked wallets). GET /api/my/trace?who=<#|username> (yourself, anyone up to 3 levels
below you, or your own 3 uplines) and GET /api/admin/trace?who= list every purchase the member was
part of, newest first: buyer, sponsor, package, then levels 1-3 with the recipient, the skipped
positions and the reason ("not qualified: had N of 2 qualifying buyers then"), the platform share, and
a verify link. Linked extra wallets are named after their owner. Earnings tab card "Trace a payment";
admin member card gets a "Payment trace" section; chatbot canned answer + AI prompt route the
question there.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
martbost
2026-09-16 12:24:53 -05:00
parent dffcaa113b
commit a727ab1ce5
6 changed files with 145 additions and 2 deletions
+25
View File
@@ -364,6 +364,31 @@
+ (d.campaigns.length ? d.campaigns.map(c => '<tr><td>' + c.id + '</td><td>' + esc(c.type) + '</td><td>' + esc(c.status) + '</td><td>' + Number(c.budget || 0).toLocaleString() + '</td><td>' + Number(c.spent || 0).toLocaleString() + '</td><td>' + Number(c.views || 0).toLocaleString() + '</td><td>' + Number(c.clicks || 0).toLocaleString() + '</td><td class="small muted">' + when(c.created) + '</td></tr>').join('') : '<tr><td colspan="8" class="muted">No campaigns.</td></tr>') + '</table></div>';
$('mcBody').innerHTML = h;
if (location.hash !== '#members') history.replaceState(null, '', '#members');
loadTrace(d);
}
// payment trace under the member card (2026-09-16): per purchase, who was paid / skipped and why
async function loadTrace(d) {
const ch = d.chain || {}; const mid = ch.memberId || (d.account && d.account.memberId) || 0;
const box = document.createElement('div'); box.id = 'mcTrace'; box.innerHTML = '<h4 style="margin:18px 0 6px">Payment trace</h4><p class="small muted">Reading the chain…</p>';
$('mcBody').appendChild(box);
if (!mid) { box.innerHTML = '<h4 style="margin:18px 0 6px">Payment trace</h4><p class="small muted">Not activated on the chain, nothing to trace.</p>'; return; }
try {
const r = await api('/api/admin/trace?who=' + mid);
const nm = id => id ? ('#' + id + (r.names[id] ? ' @' + esc(r.names[id]) : '')) : 'nobody';
let h = '<h4 style="margin:18px 0 6px">Payment trace</h4><p class="small muted" style="margin:0 0 6px">' + r.buyersNow + ' qualifying buyer' + (r.buyersNow === 1 ? '' : 's') + ' · every purchase this member was part of, newest first.</p>';
if (!r.purchases.length) h += '<p class="small muted">No purchases involving them yet.</p>';
for (const p of r.purchases) {
h += '<div style="border:1px solid rgba(255,255,255,.1);border-radius:8px;padding:8px 10px;margin:0 0 6px"><div class="small"><b>' + when(p.ts) + '</b> · ' + nm(p.buyerId) + ' bought $' + (p.priceCents / 100).toFixed(0) + ' (' + polOf(p.paidWei) + ' POL)' + (p.sponsorId ? ' · sponsor ' + nm(p.sponsorId) : '') + ' · <span class="muted">' + esc(p.tx.slice(0, 12)) + '…</span></div>';
for (const t of p.tiers) {
let line = '<b>L' + t.tier + ' ' + t.pct + '%</b>: ';
for (const x of t.skipped) line += '<span style="color:#f0a742">' + nm(x.id) + ' skipped (' + (x.reason === 'unqualified' ? 'had ' + x.buyersThen + ' of ' + (t.tier === 2 ? 2 : 5) + ' buyers then' : esc(x.reason)) + ')</span> → ';
line += t.paidTo ? '<span style="color:' + (t.paidTo === mid ? '#3ecf7a' : 'inherit') + '">' + nm(t.paidTo) + ' ' + polOf(t.amountWei) + ' POL</span>' : '<span class="muted">no catcher → company</span>';
h += '<div class="small" style="margin-top:3px">' + line + '</div>';
}
h += '<div class="small muted" style="margin-top:3px">platform ' + polOf(p.adminWei) + ' POL</div></div>';
}
box.innerHTML = h;
} catch (e) { box.innerHTML = '<h4 style="margin:18px 0 6px">Payment trace</h4><p class="small bad">' + esc(e.message) + '</p>'; }
}
function closeMember() { $('memCard').hidden = true; document.querySelectorAll('#pane-members > .card').forEach(c => { c.hidden = false; }); }
// live matches while typing: any part of the username, email, member #, share code or wallet