Admin: payment-routing / leak map for the org

New chain.getOrgRouting(rootId, ownerIds) simulates the contract's
_payUpline routing for every member below the org root on their NEXT
upgrade, classifying where the POL lands: your positions, a teammate
in-org (healthy), an outsider above the org (true LEAK), or fees.
Surfaced on /api/admin/income and rendered as a panel in "My Positions
— Income": 4-stat summary + expandable list of payments escaping the
org. Distinguishes real leaks (money leaving the team) from teammates
earning — which the old upgrade-needs alert conflated.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
martbost
2026-08-15 18:35:48 -05:00
parent 0496584fb2
commit 9717ddbd85
4 changed files with 65 additions and 3 deletions
+47 -1
View File
@@ -521,6 +521,52 @@ function getOwnerUpgradeNeeds(ids) {
return { ready: true, needs };
}
// Payment-routing / leak map: simulates the contract's _payUpline routing for
// every member below `rootId` on their NEXT upgrade, classifying where the POL
// lands — a position you own, a teammate inside your org, an outsider above the
// org (a true LEAK), or fees. Answers "is money escaping my org?".
function getOrgRouting(rootId, ownerIds) {
if (!state || !state.snapshotAt || !costs) return { ready: false };
const M = state.members;
const CROOT = 1; // contract root position (#1); payments passing it go to fees
const owned = new Set((ownerIds || []).map(Number));
const lvl = id => (M[id] && M[id].level) || 1;
const catcher = (fromId, li) => {
let up = M[fromId] && M[fromId].uplineId;
for (let i = 0; i < 16; i++) {
if (!up || up === CROOT) return null;
const u = M[up]; if (!u) return null;
if (i < li) { up = u.uplineId; continue; }
if ((u.level || 1) > li && (u.directCount || 0) >= 2) return up;
up = u.uplineId;
}
return null;
};
const inOrg = id => { let up = M[id] && M[id].uplineId, g = 0; while (up && up !== CROOT && g++ < 48) { if (up === rootId) return true; up = M[up].uplineId; } return false; };
const ids = Object.keys(M).map(Number).filter(id => id !== rootId && inOrg(id));
let ownedPol = 0, teamPol = 0, leakPol = 0, feePol = 0;
const leaks = [];
for (const id of ids) {
const L = lvl(id); if (L >= 8) continue;
const li = L - 1;
const tier = (M[id].tier === 2) ? 2 : 1;
const amt = (costs.up[tier] || [])[li] || 0;
if (!amt) continue;
const c = catcher(id, li);
if (c == null) feePol += amt;
else if (owned.has(c)) ownedPol += amt;
else if (inOrg(c)) teamPol += amt;
else { leakPol += amt; leaks.push({ from: id, to: c, toLevel: lvl(c), amt: +amt.toFixed(2) }); }
}
leaks.sort((a, b) => b.amt - a.amt);
return {
ready: true, rootId, members: ids.length,
ownedPol: +ownedPol.toFixed(2), teamPol: +teamPol.toFixed(2),
leakPol: +leakPol.toFixed(2), feePol: +feePol.toFixed(2),
leaks: leaks.slice(0, 40)
};
}
// How an organization rooted at `rootId` stacks up against the whole contract —
// share of members and share of all POL paid. Computed from in-memory state.
function getOrgShare(rootId) {
@@ -559,4 +605,4 @@ async function getIncome(id) {
};
}
module.exports = { startIndexer, getPayoutsPublic, verifyMember, memberLookup, memberPublic, getIncome, getOwnerUpgradeNeeds, getOrgShare, getMatrixTree, isInTeam, CONTRACT };
module.exports = { startIndexer, getPayoutsPublic, verifyMember, memberLookup, memberPublic, getIncome, getOwnerUpgradeNeeds, getOrgRouting, getOrgShare, getMatrixTree, isInTeam, CONTRACT };