// Circle Suite usage metering. Per-position, per-tool, per-calendar-month // counters with level-based quotas. Storage matches the house pattern: one // JSON file in the data volume, read/written on demand (low volume, human // scale — a member generating copy is not a hot path). // // Quotas rise with level because cost should scale with commitment, and the // meter is ALWAYS visible to the member: no silent throttling, no surprise // cutoffs. Hitting a cap shows what unlocks more, never a dead end. '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, 'suite-usage.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'); } // tool -> minimum level, and per-level monthly allowance (index = level-1). const TOOLS = { copy: { minLevel: 2, quota: [0, 150, 250, 400, 600, 800, 1000, 1500], label: 'Copy Engine' }, page: { minLevel: 2, quota: [0, 3, 5, 15, 25, 50, 50, 50], label: 'Page Builder' }, email: { minLevel: 3, quota: [0, 0, 100, 200, 300, 500, 500, 500], label: 'Email Engine' }, video: { minLevel: 3, quota: [0, 0, 4, 8, 12, 20, 20, 20], label: 'Video Maker' } }; function quotaFor(tool, level) { const t = TOOLS[tool]; if (!t) return 0; const lv = Math.max(1, Math.min(8, Number(level) || 1)); if (lv < t.minLevel) return 0; return t.quota[lv - 1] || 0; } function used(memberId, tool) { const all = readAll(); const m = all[monthKey()] || {}; const rec = m[String(memberId)] || {}; return Number(rec[tool] || 0); } // Can this position use this tool right now? Returns the full meter state so // the UI can render it without a second call. function check(memberId, level, tool) { const t = TOOLS[tool]; if (!t) return { allowed: false, reason: 'unknown-tool', used: 0, limit: 0, remaining: 0 }; const limit = quotaFor(tool, level); const u = used(memberId, tool); if (limit <= 0) { return { allowed: false, reason: 'locked', minLevel: t.minLevel, used: u, limit: 0, remaining: 0, label: t.label }; } const remaining = Math.max(0, limit - u); return { allowed: remaining > 0, reason: remaining > 0 ? 'ok' : 'quota', used: u, limit, remaining, label: t.label, resets: monthKey() }; } function record(memberId, tool, n) { const all = readAll(); const mk = monthKey(); if (!all[mk]) all[mk] = {}; const id = String(memberId); if (!all[mk][id]) all[mk][id] = {}; all[mk][id][tool] = Number(all[mk][id][tool] || 0) + (Number(n) || 1); // keep only the last 3 months of counters — the file stays tiny forever const keep = Object.keys(all).sort().slice(-3); const trimmed = {}; keep.forEach(function (k) { trimmed[k] = all[k]; }); writeAll(trimmed); return all[mk][id][tool]; } // Every meter for one position — powers the Suite's usage strip. function meters(memberId, level) { const out = {}; Object.keys(TOOLS).forEach(function (tool) { out[tool] = check(memberId, level, tool); }); return out; } module.exports = { init, check, record, meters, quotaFor, TOOLS };