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:
@@ -380,7 +380,8 @@ async function handleApi(req,res,pathname){
|
||||
for(const p of r.income){rows.push({toId:r.id,fromId:p.fromId,pol:p.pol,ts:p.ts,desc:p.desc});grandListed+=p.pol;}
|
||||
}
|
||||
rows.sort((a,b)=>(b.ts||0)-(a.ts||0));
|
||||
return json(res,200,{ids,perId,rows:rows.slice(0,500),grandEarnedPol:+grand.toFixed(2),grandListedPol:+grandListed.toFixed(2)});
|
||||
let upgradeNeeds=[];try{upgradeNeeds=chain.getOwnerUpgradeNeeds(ids).needs;}catch(e){}
|
||||
return json(res,200,{ids,perId,rows:rows.slice(0,500),grandEarnedPol:+grand.toFixed(2),grandListedPol:+grandListed.toFixed(2),upgradeNeeds});
|
||||
}catch(e){return json(res,502,{error:e.message||'Lookup failed'})}
|
||||
}
|
||||
if(req.method==='GET'&&pathname==='/api/admin/member-lookup'){
|
||||
@@ -413,7 +414,7 @@ async function handleApi(req,res,pathname){
|
||||
const maxOrder=sponsors.reduce((m,s)=>Math.max(m,s.sortOrder||0),0);sponsors.push({id:String(id).trim(),name:String(name).trim(),parentId:String(parentId||'').trim(),directs:0,level,status:sponsors.some(s=>s.status==='active')?'waiting':'active',sortOrder:maxOrder+10,clicks:0,notes:String(notes||'').trim(),email:String(email||'').trim().slice(0,120)});sponsors=normalizeStatuses(sponsors);saveSponsors(sponsors);return json(res,201,{sponsors});
|
||||
}
|
||||
if(req.method==='PATCH'&&pathname==='/api/admin/config'){
|
||||
const b=await bodyJson(req),cur=getConfig(),next={...cur};for(const k of ['siteName','programName','bridgeHeadline','bridgeSubheadline','premiumEntryPol','dappReferralBaseUrl','telegramUrl','supportLabel','showSponsorName','showQueueProgress','bemobPostbackUrl','telegramBotToken','telegramChatId','telegramTopicId','teamRootId','emailFrom','teamAlertEmail','ownerIds'])if(Object.prototype.hasOwnProperty.call(b,k))next[k]=b[k];next.premiumEntryPol=Number(next.premiumEntryPol)||362;next.updatedAt=new Date().toISOString();writeJson(CONFIG_FILE,next);return json(res,200,{config:next});
|
||||
const b=await bodyJson(req),cur=getConfig(),next={...cur};for(const k of ['siteName','programName','bridgeHeadline','bridgeSubheadline','premiumEntryPol','dappReferralBaseUrl','telegramUrl','supportLabel','showSponsorName','showQueueProgress','bemobPostbackUrl','telegramBotToken','telegramChatId','telegramTopicId','teamRootId','emailFrom','teamAlertEmail','ownerIds','ownerAlertEmail'])if(Object.prototype.hasOwnProperty.call(b,k))next[k]=b[k];next.premiumEntryPol=Number(next.premiumEntryPol)||362;next.updatedAt=new Date().toISOString();writeJson(CONFIG_FILE,next);return json(res,200,{config:next});
|
||||
}
|
||||
const m=pathname.match(/^\/api\/admin\/sponsors\/([^/]+)(?:\/(increment|activate|qualify|reset|move))?$/);
|
||||
if(m){const id=decodeURIComponent(m[1]),action=m[2]||null;let sponsors=getSponsors(),idx=sponsors.findIndex(s=>s.id===id);if(idx<0)return json(res,404,{error:'Sponsor not found.'});
|
||||
@@ -443,6 +444,40 @@ const server=http.createServer(async(req,res)=>{
|
||||
server.listen(PORT,()=>{console.log(`Crypto Team Build sponsor router running on http://localhost:${PORT}`);if(ADMIN_PASSWORD==='changeme')console.warn('WARNING: Set ADMIN_PASSWORD before production deployment.');});
|
||||
// Team-activity alerts: any NEW on-chain event at/below config.teamRootId goes
|
||||
// to the Telegram group topic, with the sponsor's contact email when we have it.
|
||||
// Owner upgrade watcher: emails/Telegrams when an owned position (config.ownerIds)
|
||||
// has a payment about to arrive it can't catch yet, so Marty can upgrade in time.
|
||||
const OWNER_ALERTS_FILE = path.join(DATA_DIR, 'owner-alerts.json');
|
||||
function loadOwnerAlerts(){ try{ return new Set(readJson(OWNER_ALERTS_FILE)); }catch(e){ return new Set(); } }
|
||||
function parseOwnerIds(){ return [...new Set(String(getConfig().ownerIds||'').split(',').map(s=>parseInt(String(s).trim(),10)).filter(n=>Number.isInteger(n)&&n>0))].slice(0,12); }
|
||||
function checkOwnerUpgrades(){
|
||||
try{
|
||||
const c=getConfig(); const ids=parseOwnerIds();
|
||||
if(!ids.length) return;
|
||||
const res=chain.getOwnerUpgradeNeeds(ids);
|
||||
if(!res.ready) return;
|
||||
const alerted=loadOwnerAlerts(); const active=new Set();
|
||||
for(const n of res.needs){
|
||||
const key=`${n.id}:${n.reason}:${n.neededLevel}`; active.add(key);
|
||||
if(alerted.has(key)) continue;
|
||||
alerted.add(key);
|
||||
const who=n.members.map(m=>'#'+m).join(', ');
|
||||
const action=n.reason==='qualify'
|
||||
? `Position #${n.id} needs its 2 directs to catch this.`
|
||||
: `Upgrade position #${n.id} (now ${n.levelName}) to ${n.neededLevelName} to catch it.`;
|
||||
if(c.ownerAlertEmail){
|
||||
sendEmailRaw(c.ownerAlertEmail,
|
||||
`RM Circle: upgrade #${n.id} to ${n.neededLevelName} — ${n.amountAtRisk} POL incoming`,
|
||||
`Heads up — one of your positions has money about to arrive that it can't catch yet.\n\nPosition #${n.id} is at ${n.levelName}. Member(s) ${who} are ONE upgrade away from paying #${n.id} about ${n.amountAtRisk} POL — but that payment only stops at #${n.id} if it's at ${n.neededLevelName} and qualified.\n\n${action}\n\nDo it before they upgrade, or the payment passes to the next eligible position above you (it doesn't come back). Your positions: https://rmcircle.saasy.top/admin\n\n— RM Circle auto-watch`);
|
||||
}
|
||||
sendTelegram(`⏫ UPGRADE #${n.id} SOON: ${who} one upgrade from paying ~${n.amountAtRisk} POL. #${n.id} is ${n.levelName} — needs ${n.neededLevelName}${n.reason==='qualify'?' + 2 directs':''}. Upgrade before they do.`);
|
||||
}
|
||||
let changed=false;
|
||||
for(const k of [...alerted]) if(!active.has(k)){ alerted.delete(k); changed=true; }
|
||||
if(changed||active.size) writeJson(OWNER_ALERTS_FILE,[...alerted]);
|
||||
}catch(e){ console.error('owner upgrade check', e.message); }
|
||||
}
|
||||
setInterval(checkOwnerUpgrades, 5*60*1000).unref();
|
||||
setTimeout(checkOwnerUpgrades, 30000).unref();
|
||||
chain.startIndexer(evt=>{
|
||||
try{
|
||||
const c=getConfig();
|
||||
|
||||
Reference in New Issue
Block a user