// Circle Suite — Team Grants (Vertex L7, Corona L8). // // The problem this fixes: every tool from L1 to L8 was a "me" tool — help ME // write, help ME post, help ME measure. But past the first couple of levels the // business is not about you. It is get two, help your two, teach them to teach. // So the top of the ladder was scaling the wrong axis: more of your own // capability, at double the price each rung. // // A grant lets a leader hand Suite capacity DOWN into their own organisation — // impressions, generations, page builds, ad batches. Three things make it work: // // 1. It comes from a POOL that is a perk of the level, not a tax on the // leader's own allowance. Making leaders choose between equipping their // team and using their own tools would just mean nobody grants anything. // 2. It can only go to someone inside the grantor's own organisation, checked // against the contract. This is not a way to hand capacity to strangers. // 3. It expires with the month, like every other allowance here. Grants are // for unblocking someone now, not for building up a balance. 'use strict'; const fs = require('fs'); const path = require('path'); let DATA_DIR = null; function init(opts) { DATA_DIR = opts.dataDir; } function file() { return path.join(DATA_DIR, 'team-grants.json'); } function readAll() { try { return JSON.parse(fs.readFileSync(file(), 'utf8')); } catch (e) { return {}; } } function writeAll(v) { try { fs.writeFileSync(file(), JSON.stringify(v)); } catch (e) {} } function monthKey() { const d = new Date(); return d.getUTCFullYear() + '-' + String(d.getUTCMonth() + 1).padStart(2, '0'); } const MIN_LEVEL = 7; // What a leader can give away each month, by level (index = level - 1). // Zero below Vertex — this is the thing the top of the ladder buys. const POOLS = { traffic: { label: 'Ad impressions', unit: 'impressions', step: 1000, max: 50000, pool: [0, 0, 0, 0, 0, 0, 60000, 200000] }, copy: { label: 'Copy Engine generations', unit: 'generations', step: 25, max: 300, pool: [0, 0, 0, 0, 0, 0, 300, 1000] }, page: { label: 'Page builds', unit: 'pages', step: 1, max: 20, pool: [0, 0, 0, 0, 0, 0, 15, 50] }, textad: { label: 'Text ad batches', unit: 'batches', step: 5, max: 60, pool: [0, 0, 0, 0, 0, 0, 60, 200] } }; function tools() { return Object.keys(POOLS).map(function (k) { return { key: k, label: POOLS[k].label, unit: POOLS[k].unit, step: POOLS[k].step, max: POOLS[k].max }; }); } // A tester on a level override can SEE every tier, but must not be able to // hand out a real Corona pool — 200,000 network impressions given to real // members is real inventory, spent for real. Preview pools are big enough to // exercise the whole flow and small enough that losing them costs nothing. const PREVIEW_POOL = { traffic: 5000, copy: 25, page: 2, textad: 5 }; function poolFor(tool, level, preview) { const t = POOLS[tool]; if (!t) return 0; const lv = Math.max(1, Math.min(8, Number(level) || 1)); const base = t.pool[lv - 1] || 0; if (preview) return Math.min(base, PREVIEW_POOL[tool] || 0); return base; } function rows(mk) { const all = readAll(); return all[mk || monthKey()] || []; } // How much of `tool` this position has RECEIVED this month. function receivedBy(memberId, tool) { return rows().reduce(function (s, r) { return s + ((Number(r.to) === Number(memberId) && r.tool === tool) ? Number(r.n) || 0 : 0); }, 0); } // How much of `tool` this position has GIVEN AWAY this month. function givenBy(memberId, tool) { return rows().reduce(function (s, r) { return s + ((Number(r.by) === Number(memberId) && r.tool === tool) ? Number(r.n) || 0 : 0); }, 0); } // Who gave it to them — so a recipient can see it came from their upline and // not from nowhere. That is half the point: it should feel like being helped. function receivedDetail(memberId) { const out = {}; rows().forEach(function (r) { if (Number(r.to) !== Number(memberId)) return; if (!out[r.tool]) out[r.tool] = { total: 0, from: [] }; out[r.tool].total += Number(r.n) || 0; out[r.tool].from.push({ by: Number(r.by), n: Number(r.n), at: r.at }); }); return out; } function status(memberId, level, preview) { const t = {}; Object.keys(POOLS).forEach(function (k) { const pool = poolFor(k, level, preview); const given = givenBy(memberId, k); t[k] = { label: POOLS[k].label, unit: POOLS[k].unit, step: POOLS[k].step, max: POOLS[k].max, pool: pool, given: given, remaining: Math.max(0, pool - given) }; }); return { level: Number(level) || 0, canGrant: Number(level) >= MIN_LEVEL, preview: !!preview, tools: t, resets: monthKey(), history: rows().filter(function (r) { return Number(r.by) === Number(memberId); }).slice(-40).reverse(), received: receivedDetail(memberId) }; } // Hand capacity to someone. The caller is responsible for having verified that // `to` is inside `by`'s organisation — that check needs the chain and lives in // the route, not here. function grant(opts) { const by = Number(opts.by), to = Number(opts.to); const tool = String(opts.tool || ''); const n = Math.floor(Number(opts.n) || 0); const level = Number(opts.level) || 0; if (!POOLS[tool]) throw new Error('That is not something you can grant.'); if (level < MIN_LEVEL) throw new Error('Team Grants unlock at Vertex (level 7).'); if (!to || to === by) throw new Error('Pick someone in your team other than yourself.'); if (n <= 0) throw new Error('Choose how much to give.'); if (n > POOLS[tool].max) throw new Error('The most you can give one person at a time is ' + POOLS[tool].max.toLocaleString() + '.'); const pool = poolFor(tool, level, opts.preview); const given = givenBy(by, tool); if (given + n > pool) { throw new Error('That is more than you have left to give this month — ' + Math.max(0, pool - given).toLocaleString() + ' ' + POOLS[tool].unit + ' remaining.'); } const all = readAll(); const mk = monthKey(); if (!all[mk]) all[mk] = []; const rec = { by: by, to: to, tool: tool, n: n, at: new Date().toISOString() }; all[mk].push(rec); // Keep three months, same as every other ledger here. const keep = Object.keys(all).sort().slice(-3); const trimmed = {}; keep.forEach(function (k) { trimmed[k] = all[k]; }); writeAll(trimmed); return rec; } module.exports = { init, MIN_LEVEL, POOLS, tools, poolFor, status, grant, receivedBy, givenBy, receivedDetail };