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:
@@ -0,0 +1,93 @@
|
||||
// Where a member's personal link places the people who use it.
|
||||
//
|
||||
// Two settings, chosen by the member:
|
||||
//
|
||||
// 'team' — the moving link. A join is routed to the next position IN THEIR OWN LEG that
|
||||
// still needs directs. This is rotation inside their downline, never the company
|
||||
// rotation: a member's personal link must never hand their referral to someone
|
||||
// they have never met. (Marty, 2026-09-18 — exactly what went wrong for Terry
|
||||
// #840, whose link was offering company position #148.)
|
||||
// 'direct' — every join lands directly under them, as depth in their own leg.
|
||||
//
|
||||
// A new member STARTS on whatever their sponsor was using at the moment they joined — so a
|
||||
// team duplicates its leader without anyone being locked in. That is a one-time copy, not a
|
||||
// live link: if the sponsor changes their mind later, nobody already in the team moves. An
|
||||
// explicit choice is never overwritten from above.
|
||||
//
|
||||
// `explicit` records whether the member chose it or simply inherited it. It is what lets the
|
||||
// dashboard say where the setting came from, and it is why a third "follow my sponsor" state
|
||||
// was not needed.
|
||||
'use strict';
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
let DATA_DIR = null;
|
||||
let FILE = null;
|
||||
let db = null; // { ids: () => configured always-direct ids }
|
||||
|
||||
function init(opts) {
|
||||
DATA_DIR = opts.dataDir;
|
||||
FILE = path.join(DATA_DIR, 'placement.json');
|
||||
db = opts;
|
||||
}
|
||||
|
||||
function load() {
|
||||
try { return JSON.parse(fs.readFileSync(FILE, 'utf8')) || {}; } catch (e) { return {}; }
|
||||
}
|
||||
function save(o) {
|
||||
try {
|
||||
const tmp = FILE + '.tmp';
|
||||
fs.writeFileSync(tmp, JSON.stringify(o));
|
||||
fs.renameSync(tmp, FILE);
|
||||
} catch (e) { console.error('placement save', e.message); }
|
||||
}
|
||||
|
||||
const MODES = ['team', 'direct'];
|
||||
const clean = m => (MODES.includes(String(m)) ? String(m) : null);
|
||||
|
||||
// The admin list stays as a support override and as the migration path for the positions that
|
||||
// were on it before this existed. An entry there reads as an explicit 'direct'.
|
||||
function adminDirectIds(cfg) {
|
||||
return String((cfg && cfg.directDefaultIds) || '')
|
||||
.split(',').map(s => s.trim()).filter(Boolean);
|
||||
}
|
||||
|
||||
// What is this position set to, and where did that come from?
|
||||
function get(id, cfg) {
|
||||
const key = String(id);
|
||||
const rec = load()[key];
|
||||
if (rec && clean(rec.mode)) {
|
||||
return { mode: rec.mode, explicit: !!rec.explicit, from: rec.from || null, ts: rec.ts || null };
|
||||
}
|
||||
if (adminDirectIds(cfg).includes(key)) {
|
||||
return { mode: 'direct', explicit: true, from: 'admin', ts: null };
|
||||
}
|
||||
return { mode: 'team', explicit: false, from: null, ts: null };
|
||||
}
|
||||
|
||||
// The member chose it themselves.
|
||||
function set(id, mode) {
|
||||
const m = clean(mode);
|
||||
if (!m) return { error: 'Pick either team or direct.' };
|
||||
const o = load();
|
||||
o[String(id)] = { mode: m, explicit: true, from: null, ts: Date.now() };
|
||||
save(o);
|
||||
return { ok: true, ...get(id) };
|
||||
}
|
||||
|
||||
// A new position inherits its sponsor's CURRENT setting, once, at join. Never overwrites a
|
||||
// member who has already chosen, and never touches anyone else in the leg.
|
||||
function inherit(newId, sponsorId, cfg) {
|
||||
const key = String(newId);
|
||||
const o = load();
|
||||
if (o[key] && o[key].explicit) return { skipped: 'already chosen' };
|
||||
const parent = get(sponsorId, cfg);
|
||||
if (parent.mode === 'team') { // the default; no row needed
|
||||
if (!o[key]) return { skipped: 'default' };
|
||||
}
|
||||
o[key] = { mode: parent.mode, explicit: false, from: String(sponsorId), ts: Date.now() };
|
||||
save(o);
|
||||
return { ok: true, mode: parent.mode, from: String(sponsorId) };
|
||||
}
|
||||
|
||||
module.exports = { init, get, set, inherit, MODES };
|
||||
Reference in New Issue
Block a user