Credit activity ledger: every earn and spend logged with a reason, listed under Campaigns
Hugh earned a 10-credit sign-in bonus, then bought a 280-credit extension; earned credits spend first, so Earned read 0 and the bonus looked lost. There was no record a member could check. Now every real credit movement writes a credit_log row (JSON store in file mode): welcome, sign-in bonus with streak, daily claim, inbox reads, video watches, verified visits, milestone/leaderboard bonuses, partner codes, admin/team grants, AI Copy Engine charges + refunds, flat buys (featured run, extension, visit pack, more visits), login-ad days, and metered delivery rolled up per campaign per day; on-chain purchased-credit spends are marked. GET /api/my/credits/activity; dashboard shows the last 40 under Campaigns (also when there are no campaigns) with a link from Earn credits. Chatbot canned answer + AI prompt updated; my.js tag bumped. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
@@ -1048,11 +1048,11 @@ const server = http.createServer(async (req, res) => {
|
||||
// partner promo code carried on the join link: redeem once per account (ignored if invalid/used)
|
||||
try {
|
||||
const pc = parseCookies(req)['iap.promo'];
|
||||
if (pc) { const g = await promos.redeem(pc, e, 'link'); if (g.ok) { if (g.funder && g.funder !== e) { if (await ads.spendEarned(g.funder, g.credits)) { await ads.addEarned(e, g.credits); console.log('promo redeemed (member-funded)', g.code, g.credits, e, 'by', g.funder); } else console.log('promo funder short', g.code, g.funder); } else { await ads.addEarned(e, g.credits); console.log('promo redeemed', g.code, g.credits, e); } } }
|
||||
if (pc) { const g = await promos.redeem(pc, e, 'link'); if (g.ok) { if (g.funder && g.funder !== e) { if (await ads.spendEarned(g.funder, g.credits, { log: { kind: 'promo', note: 'Partner code ' + g.code + ' funded for ' + e } })) { await ads.addEarned(e, g.credits, { log: { kind: 'promo', note: 'Partner code ' + g.code } }); console.log('promo redeemed (member-funded)', g.code, g.credits, e, 'by', g.funder); } else console.log('promo funder short', g.code, g.funder); } else { await ads.addEarned(e, g.credits, { log: { kind: 'promo', note: 'Partner code ' + g.code } }); console.log('promo redeemed', g.code, g.credits, e); } } }
|
||||
} catch (err) { console.error('promo redeem', err.message); }
|
||||
// legacy bridge: a listed former Faucet Wave / Tier One Ads member gets welcome-back credits once
|
||||
if (r.created && /^(fw|t1)-(adv|earn)$/.test(via)) {
|
||||
try { const g = legacy.grant(e, siteConfig()); if (g) { await ads.addEarned(e, g.credits); console.log('legacy grant', g.brand, g.seg, g.credits, e); } }
|
||||
try { const g = legacy.grant(e, siteConfig()); if (g) { await ads.addEarned(e, g.credits, { log: { kind: 'grant', note: 'Welcome-back credits (' + g.brand + ')' } }); console.log('legacy grant', g.brand, g.seg, g.credits, e); } }
|
||||
catch (err) { console.error('legacy grant', err.message); }
|
||||
}
|
||||
if (r.created && b.newsletter) sendy.subscribe(r.account.email, r.account.username || '').catch(() => {}); // pre-checked opt-in, silent, new joins only
|
||||
@@ -1300,7 +1300,7 @@ const server = http.createServer(async (req, res) => {
|
||||
const b = await readBody(req);
|
||||
const g = await promos.redeem(b.code, s.email, 'dashboard');
|
||||
if (g.error) return json(res, 400, g);
|
||||
await ads.addEarned(s.email, g.credits);
|
||||
await ads.addEarned(s.email, g.credits, { log: { kind: 'promo', note: 'Partner code ' + String(b.code || '').toUpperCase() } });
|
||||
return json(res, 200, { ok: true, credits: g.credits, code: g.code, partner: g.partner });
|
||||
}
|
||||
// -- admin: member card. GET ?q= resolves email / @username / #id / share code / wallet;
|
||||
@@ -1336,7 +1336,7 @@ const server = http.createServer(async (req, res) => {
|
||||
if (b.grantCredits !== undefined) {
|
||||
const n = Math.round(Number(b.grantCredits));
|
||||
if (!(n > 0) || n > 100000) return json(res, 400, { error: 'Credits: a whole number from 1 to 100,000.' });
|
||||
await ads.addEarned(e, n);
|
||||
await ads.addEarned(e, n, { log: { kind: 'grant', note: 'Credits granted by admin' } });
|
||||
console.log('admin credits', e, n, String(b.note || '').slice(0, 100));
|
||||
}
|
||||
return json(res, 200, await adminMember.view(e));
|
||||
@@ -2259,7 +2259,7 @@ const server = http.createServer(async (req, res) => {
|
||||
return json(res, 200, { ok: true, credited: 0, status: await ads.videoStatus(s.email), already: true });
|
||||
const tier = await ads.chargeVideoView(t.id); // charge advertiser; null if it ran dry
|
||||
if (!tier) return json(res, 200, { ok: true, credited: 0, status: await ads.videoStatus(s.email), gone: true });
|
||||
await ads.addEarned(s.email, tier.reward);
|
||||
await ads.addEarned(s.email, tier.reward, { log: { kind: 'earn', note: 'Watched a video (' + tier.secs + 's)' } });
|
||||
await ads.markVideoSeen(s.email, t.id);
|
||||
const status = await ads.recordVideoWatch(s.email);
|
||||
return json(res, 200, { ok: true, credited: tier.reward, status });
|
||||
@@ -2379,6 +2379,11 @@ const server = http.createServer(async (req, res) => {
|
||||
res.writeHead(302, baseHeaders({ Location: target }));
|
||||
return res.end();
|
||||
}
|
||||
if (p === '/api/my/credits/activity' && req.method === 'GET') { // credit ledger: every earn and spend with a reason, newest first
|
||||
const s = await auth.fromRequest(req);
|
||||
if (!s || !s.email) return json(res, 401, { error: 'Sign in first.' });
|
||||
return json(res, 200, { items: await ads.creditActivity(s.email, 40) });
|
||||
}
|
||||
if (p === '/api/my/campaigns' && req.method === 'GET') {
|
||||
const s = await auth.fromRequest(req);
|
||||
if (!s || !s.email) return json(res, 401, { error: 'Sign in first.' });
|
||||
|
||||
Reference in New Issue
Block a user