Self-rotating personal links: qualified member's join routes to next-to-qualify

Fixes the spillover leak where a qualified member promoting their own link had
joins land back on themselves (bonus reward) instead of qualifying their team.

- /api/public/member now returns joinTarget: unqualified member -> self;
  qualified -> next-to-qualify in their leg (reuses rotation-aligned nextInLine);
  whole leg qualified -> global rotation sponsor; else -> spillover (prior behavior)
- /join page: the Join button, "join under" header, sponsor-ID copy, and the
  submitted sponsorId all follow joinTarget; a note explains "invited by #X,
  joining to help #Y qualify" so the new member knows their real sponsor
- Member's SHARE link is unchanged (/join/<id>); resolution happens when a
  visitor opens it, so it self-corrects even for links already in the wild

Dry-run verified: /join/27 -> #34, /join/34 -> #34 (self), /join/21 -> #34.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
martbost
2026-08-16 13:02:34 -05:00
parent 19751a6561
commit f0ea4469a4
2 changed files with 40 additions and 7 deletions
+22
View File
@@ -363,6 +363,28 @@ async function handleApi(req,res,pathname){
}catch(e){}
}
if(r.nextInLine)r.nextInLine.referralUrl=`${getConfig().dappReferralBaseUrl}${encodeURIComponent(r.nextInLine.id)}`;
// Moving-link routing: where a join through THIS member's personal link
// actually lands. Not-yet-qualified -> their own position (still filling
// their 2). Qualified -> the next-to-qualify in their leg (nextInLine,
// already aligned to the curated rotation above), so their promotion
// builds the team down in order instead of spilling onto themselves.
// Whole leg qualified -> the global rotation sponsor. Last resort -> their
// own position (spillover, the prior behavior).
if(r.registered){
const base=getConfig().dappReferralBaseUrl;
if((r.directCount||0)<2){
r.joinTarget={id,referralUrl:r.referralUrl,reason:'self'};
}else if(r.nextInLine){
r.joinTarget={id:r.nextInLine.id,referralUrl:r.nextInLine.referralUrl,invitedBy:id,reason:'leg',directCount:r.nextInLine.directCount};
}else{
let g=null;try{g=activeSponsor(getSponsors());}catch(e){}
if(g&&String(g.id)!==String(id)&&(g.directs||0)<2){
r.joinTarget={id:g.id,referralUrl:`${base}${encodeURIComponent(g.id)}`,invitedBy:id,reason:'global',directCount:g.directs};
}else{
r.joinTarget={id,referralUrl:r.referralUrl,reason:'spillover'};
}
}
}
memberCache.set(id,{data:r,ts:Date.now()});
if(memberCache.size>500)memberCache.delete(memberCache.keys().next().value);
return json(res,200,r,{'Cache-Control':'public, max-age=60'});