Welcome credits (spec 8b earned pool v1) + official rehearsal rate card
Every account gets welcomeCredits (25, rate-card configurable) via an idempotent lazy grant on dashboard load, so existing members receive theirs retroactively. earned_credits table with JSON fallback; Overview stat shows the purchased/welcome split. Chatbot facts and canned answers synced; homepage free-membership list mentions the welcome batch. Rate card promoted from placeholder to decided and persisted in the volume. Spending integration for the earned pool is the next engine build. Assets v=20260905b. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -20,7 +20,8 @@ function rates() {
|
||||
bannerBatch: 10, bannerCreditsPerBatch: 2,
|
||||
textBatch: 10, textCreditsPerBatch: 1,
|
||||
loginCreditsPerDay: 100,
|
||||
burnBatchMin: 50
|
||||
burnBatchMin: 50,
|
||||
welcomeCredits: 25 // PLACEHOLDER until Marty sets the rate card (spec §8b earned pool)
|
||||
}, saved);
|
||||
}
|
||||
function setRates(patch) {
|
||||
@@ -240,6 +241,43 @@ const D = {
|
||||
}
|
||||
};
|
||||
|
||||
// ---- earned/welcome credits (spec §8b second pool: engine-side, never
|
||||
// on-chain; welcome grant is idempotent and lazy so existing accounts get
|
||||
// theirs on next dashboard load) ----
|
||||
const EJ = {
|
||||
db: null,
|
||||
FILE: () => path.join(DATA_DIR, 'earned.json'),
|
||||
load() { try { this.db = JSON.parse(fs.readFileSync(this.FILE(), 'utf8')); } catch (e) { this.db = {}; } },
|
||||
save() { try { fs.writeFileSync(this.FILE(), JSON.stringify(this.db)); } catch (e) {} }
|
||||
};
|
||||
async function earnedBalance(email) {
|
||||
const e = String(email || '').toLowerCase();
|
||||
if (!e) return 0;
|
||||
if (db.enabled()) {
|
||||
const r = await db.q('SELECT balance FROM earned_credits WHERE email=?', [e]);
|
||||
return r.length ? r[0].balance : 0;
|
||||
}
|
||||
if (!EJ.db) EJ.load();
|
||||
return (EJ.db[e] && EJ.db[e].balance) || 0;
|
||||
}
|
||||
async function grantWelcome(email) {
|
||||
const e = String(email || '').toLowerCase();
|
||||
if (!e) return 0;
|
||||
const amount = rates().welcomeCredits || 0;
|
||||
if (db.enabled()) {
|
||||
await db.q(`INSERT INTO earned_credits (email,balance,granted_welcome,updated) VALUES (?,?,1,?)
|
||||
ON DUPLICATE KEY UPDATE balance = balance + IF(granted_welcome=0, ?, 0),
|
||||
granted_welcome = 1, updated = VALUES(updated)`, [e, amount, Date.now(), amount]);
|
||||
return earnedBalance(e);
|
||||
}
|
||||
if (!EJ.db) EJ.load();
|
||||
if (!EJ.db[e] || !EJ.db[e].welcomed) {
|
||||
EJ.db[e] = { balance: ((EJ.db[e] && EJ.db[e].balance) || 0) + amount, welcomed: true };
|
||||
EJ.save();
|
||||
}
|
||||
return (EJ.db[e] && EJ.db[e].balance) || 0;
|
||||
}
|
||||
|
||||
const impl = () => db.enabled() ? D : J;
|
||||
function init(opts) { DATA_DIR = opts.dataDir; chain = opts.chain; J.load(); }
|
||||
|
||||
@@ -266,4 +304,4 @@ async function pendingBurns() { return impl().pendingBurns(); }
|
||||
async function markBurned(id, tx) { return impl().markBurned(id, tx); }
|
||||
|
||||
module.exports = { init, rates, setRates, createCampaign, listCampaigns, setStatus,
|
||||
serve, click, dailySweep, availableCredits, pendingBurns, markBurned };
|
||||
serve, click, dailySweep, availableCredits, earnedBalance, grantWelcome, pendingBurns, markBurned };
|
||||
|
||||
Reference in New Issue
Block a user