Members choose how their link places people
Terry asked how a referral of his would be set up as "Direct with
spillover", and whether his team could follow suit. Two settings, on the
member's own dashboard:
Team rotation — the join goes to the next position IN THEIR OWN TEAM
that still needs its 2
Direct — every join lands directly under them, as depth
A new position starts on whatever its SPONSOR was using at the moment it
joined, so a team duplicates its leader by default. That is a one-time
copy, not a live link: a sponsor changing their mind later never moves
anyone already in their team, and an explicit choice is never overwritten
from above. The panel says where an inherited setting came from, so nobody
discovers months later that their link was doing something they did not
pick.
Also closed the hole behind Terry's original complaint: a member's
personal link no longer falls through to the COMPANY rotation when their
leg is fully qualified. Team rotation means rotation inside their own
downline and nothing else — handing someone's referral to a position they
have never met was never the intent, and it is what offered Terry's
prospects #148.
The five positions already on directDefaultIds (21, 137, 139, 30, 840)
read as explicit Direct, so nothing changes for them. Everyone else stays
on Team rotation, which is what they have today.
qa/placement.mjs, 17 checks: defaults, choosing, inheritance from the
sponsor, explicit choices surviving a re-run, a sponsor switching not
moving their existing team, and persistence across a restart.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -6,6 +6,7 @@ const { URL } = require('url');
|
||||
const chain = require('./chain');
|
||||
const messages = require('./messages');
|
||||
const profiles = require('./profiles');
|
||||
const placement = require('./placement');
|
||||
const tweet = require('./tweet');
|
||||
|
||||
const PORT = Number(process.env.PORT || 3000);
|
||||
@@ -19,6 +20,7 @@ const ADMIN_PASSWORD = process.env.ADMIN_PASSWORD || 'changeme';
|
||||
const IS_PROD = process.env.NODE_ENV === 'production';
|
||||
messages.init({ dataDir: DATA_DIR, chain, isProd: IS_PROD });
|
||||
profiles.init({ dataDir: DATA_DIR, sendEmail: sendEmailRaw });
|
||||
placement.init({ dataDir: DATA_DIR });
|
||||
const suiteMeter = require('./suite-meter'); suiteMeter.init({ dataDir: DATA_DIR });
|
||||
const suiteAI = require('./suite-ai'); suiteAI.init({ dataDir: DATA_DIR });
|
||||
const suitePages = require('./suite-pages'); suitePages.init({ dataDir: DATA_DIR });
|
||||
@@ -728,12 +730,12 @@ async function handleApi(req,res,pathname){
|
||||
}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'};
|
||||
}
|
||||
// Whole leg qualified: the join lands on the member themselves as depth. It does NOT
|
||||
// fall through to the COMPANY rotation — that would hand a member's own referral to
|
||||
// a position they have never met. "Team rotation" means rotation inside their own
|
||||
// downline, and nothing else. (Marty, 2026-09-18; this fall-through is what offered
|
||||
// Terry #840's prospects company position #148.)
|
||||
r.joinTarget={id,referralUrl:r.referralUrl,reason:'spillover'};
|
||||
}
|
||||
}
|
||||
// Positions that default to DIRECT placement. A moving link is team-first:
|
||||
@@ -742,8 +744,9 @@ async function handleApi(req,res,pathname){
|
||||
// built out that is backwards - this flips the default so /join/<id> behaves
|
||||
// as ?direct=1 unless ?direct=0 is passed explicitly.
|
||||
try{
|
||||
const dd=String((getConfig().directDefaultIds)||'').split(',').map(x=>x.trim()).filter(Boolean);
|
||||
r.directDefault = dd.includes(String(id));
|
||||
const pl=placement.get(id,getConfig());
|
||||
r.directDefault = pl.mode==='direct';
|
||||
r.placement = pl; // mode + whether they chose it + which sponsor it came from
|
||||
}catch(e){ r.directDefault = false; }
|
||||
|
||||
// Next-step plan + funded badge. Wallet balance is checked server-side and
|
||||
@@ -1530,6 +1533,18 @@ async function handleApi(req,res,pathname){
|
||||
if(!s)return json(res,200,{ok:true,signedIn:false});
|
||||
return json(res,200,Object.assign({ok:true,id:s.id,suggest:profiles.suggest(s.id)},profiles.status(s.id)));
|
||||
}
|
||||
if(req.method==='GET'&&pathname==='/api/public/placement'){
|
||||
const pid=new URL(req.url,'http://x').searchParams.get('id');
|
||||
if(!/^[0-9]{1,15}$/.test(String(pid||'')))return json(res,400,{error:'id required'});
|
||||
return json(res,200,placement.get(pid,getConfig()));
|
||||
}
|
||||
if(req.method==='POST'&&pathname==='/api/public/placement'){
|
||||
const ps=messages.authFromCookie(req);
|
||||
if(!ps)return json(res,401,{error:'Not signed in.'});
|
||||
const pb=await bodyJson(req).catch(()=>null);
|
||||
const pr=placement.set(ps.id,pb&&pb.mode);
|
||||
return json(res,pr.error?400:200,pr);
|
||||
}
|
||||
if(req.method==='POST'&&pathname==='/api/public/profile/username'){
|
||||
const s=messages.authFromCookie(req);
|
||||
if(!s)return json(res,401,{error:'Not signed in.'});
|
||||
@@ -1915,6 +1930,9 @@ chain.startIndexer(evt=>{
|
||||
try{
|
||||
const c=getConfig();
|
||||
const EK=tgEventKey(evt); // one id for this on-chain event, so no feed can post it twice
|
||||
// A new position starts on its sponsor's CURRENT setting - a one-time copy, so a team
|
||||
// duplicates its leader without anyone already in it being moved later.
|
||||
if(evt.type==='registered'&&evt.referrerId){ try{ placement.inherit(evt.id,evt.referrerId,c); }catch(e){} }
|
||||
// teamRootId accepts a comma list ("21,136") — alerts fire for ANY listed org
|
||||
const roots=String(c.teamRootId||'').split(',').map(n=>Number(n.trim())).filter(n=>n>0);
|
||||
// A brand-new member has no uplineId yet at the moment the 'registered'
|
||||
|
||||
Reference in New Issue
Block a user