Alert when an owned position needs upgrading to catch incoming pay
New owner upgrade watcher: every 5 min it checks config.ownerIds against the chain state and, when a leg member is ONE upgrade from paying a position that isn't eligible yet (below the required level, or not qualified), emails config.ownerAlertEmail + pings Telegram — "upgrade #24 to Fabrica, #61 is one upgrade from paying you 2,486 POL." Deduped per (position, level) in owner-alerts.json, re-fires if the situation recurs. Same warning renders inline in the admin "My Positions" panel (upgradeNeeds in the income endpoint). New ownerAlertEmail settings field. Detection logic unit-tested. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -477,6 +477,43 @@ async function memberPublic(id) {
|
||||
return out;
|
||||
}
|
||||
|
||||
// For owned positions: detect when a leg member is ONE upgrade away from paying
|
||||
// the position, but the position isn't eligible yet (not qualified, or below the
|
||||
// required level) — i.e. "upgrade now or the payment passes you". Computed from
|
||||
// in-memory state, no RPC. A member M at depth D pays this position on M's
|
||||
// upgrade OUT of level D, so the trigger is M.level === D with the owner ineligible.
|
||||
function getOwnerUpgradeNeeds(ids) {
|
||||
if (!state || !state.snapshotAt || !costs) return { ready: false, needs: [] };
|
||||
const needs = [];
|
||||
for (const id of ids) {
|
||||
const p = state.members[id];
|
||||
const root = getSubtree(id, 99);
|
||||
if (!p || !root) continue;
|
||||
const pLevel = p.level || 1, pQual = (p.directCount || 0) >= 2;
|
||||
const items = [];
|
||||
(function walk(n, depth) {
|
||||
if (!n) return;
|
||||
if (depth >= 1 && (n.level || 1) === depth) {
|
||||
const eligible = pQual && pLevel >= depth;
|
||||
if (!eligible) items.push({ memberId: n.id, depth, amount: (costs.up[n.tier === 2 ? 2 : 1] || [])[depth - 1] || 0 });
|
||||
}
|
||||
walk(n.left, depth + 1); walk(n.right, depth + 1);
|
||||
})(root, 0);
|
||||
if (items.length) {
|
||||
const minDepth = Math.min(...items.map(i => i.depth));
|
||||
const atMin = items.filter(i => i.depth === minDepth);
|
||||
needs.push({
|
||||
id, level: pLevel, levelName: levelName(pLevel), qualified: pQual,
|
||||
neededLevel: minDepth, neededLevelName: levelName(minDepth),
|
||||
members: atMin.map(i => i.memberId),
|
||||
amountAtRisk: +atMin.reduce((s, i) => s + i.amount, 0).toFixed(2),
|
||||
reason: pQual ? 'upgrade' : 'qualify'
|
||||
});
|
||||
}
|
||||
}
|
||||
return { ready: true, needs };
|
||||
}
|
||||
|
||||
// focused income read for one position — for the admin "my positions" income view
|
||||
async function getIncome(id) {
|
||||
const m = await fetchMember(id);
|
||||
@@ -491,4 +528,4 @@ async function getIncome(id) {
|
||||
};
|
||||
}
|
||||
|
||||
module.exports = { startIndexer, getPayoutsPublic, verifyMember, memberLookup, memberPublic, getIncome, getMatrixTree, isInTeam, CONTRACT };
|
||||
module.exports = { startIndexer, getPayoutsPublic, verifyMember, memberLookup, memberPublic, getIncome, getOwnerUpgradeNeeds, getMatrixTree, isInTeam, CONTRACT };
|
||||
|
||||
Reference in New Issue
Block a user