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:
@@ -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 };
|
||||
|
||||
Reference in New Issue
Block a user