68b08ab8cc
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
151 lines
9.8 KiB
JavaScript
151 lines
9.8 KiB
JavaScript
// Five Dollar Friday (Marty, 2026-09-20). Every Friday, Central time, any ad package of $5 or more
|
||
// earns +20% bonus credits (purchased-grade, so they run login ads too), credited the moment the
|
||
// purchase indexes and labelled in the credit log. The day's wave is shown live (landing strip,
|
||
// Overview card, /api/friday), posted to Telegram at every ten packs and wrapped up at midnight,
|
||
// and five Fridays in a row earn the Five Fridays badge. Thursday 6 PM CT: an on-site notice and
|
||
// an email to active members (drip unsubscribes honoured). Friday 9 AM CT: the Telegram kickoff.
|
||
//
|
||
// Settings (siteConfig): fridayPromo '1'|'0', fridayBonusPct (20), fridayStart 'YYYY-MM-DD' (first
|
||
// Friday, 2026-09-25). Everything reads the chain index, never a marketing counter.
|
||
'use strict';
|
||
const fs = require('fs');
|
||
const path = require('path');
|
||
|
||
let X = {}, FILE = null, S = null;
|
||
const TZ = 'America/Chicago';
|
||
const dayOf = ts => new Date(ts).toLocaleDateString('en-CA', { timeZone: TZ });
|
||
const weekdayOf = ts => new Date(ts).toLocaleDateString('en-US', { timeZone: TZ, weekday: 'short' });
|
||
const hourOf = ts => Number(new Date(ts).toLocaleString('en-US', { timeZone: TZ, hour: 'numeric', hour12: false }));
|
||
const isFriday = ts => weekdayOf(ts) === 'Fri';
|
||
|
||
function init(opts) { X = opts; FILE = path.join(opts.dataDir, 'friday.json'); S = load(); }
|
||
function load() { try { return JSON.parse(fs.readFileSync(FILE, 'utf8')); } catch (e) { return { granted: {}, posts: {}, badges: {}, sent: {} }; } }
|
||
function save() { try { fs.writeFileSync(FILE, JSON.stringify(S)); } catch (e) {} }
|
||
|
||
function cfg() {
|
||
const c = (X.siteConfig && X.siteConfig()) || {};
|
||
return {
|
||
on: String(c.fridayPromo == null ? '1' : c.fridayPromo) === '1',
|
||
pct: Math.max(0, Number(c.fridayBonusPct) || 20),
|
||
start: String(c.fridayStart || '2026-09-25'),
|
||
};
|
||
}
|
||
// the next Friday on or after today (Central), as a day key and a label
|
||
function nextFriday(now) {
|
||
const t0 = now || Date.now();
|
||
for (let d = 0; d < 8; d++) { const ts = t0 + d * 86400000; if (isFriday(ts) && dayOf(ts) >= cfg().start) return dayOf(ts); }
|
||
return null;
|
||
}
|
||
function labelOf(dayKey) { const [y, m, d] = dayKey.split('-').map(Number); return new Date(Date.UTC(y, m - 1, d, 12)).toLocaleDateString('en-US', { month: 'long', day: 'numeric', timeZone: 'UTC' }); }
|
||
// is the promo live right now (a Friday on or after the start, switched on)
|
||
function liveNow(now) { const c = cfg(); const t = now || Date.now(); return c.on && isFriday(t) && dayOf(t) >= c.start; }
|
||
function counts(dayKey) { return purchasesOn(dayKey).length; }
|
||
|
||
// every $5+ purchase on a given Central day, from the chain index
|
||
function purchasesOn(dayKey) {
|
||
const out = [];
|
||
try { for (const ev of X.chain.recentEvents(1e9)) if (ev.type === 'Purchase' && ev.priceCents >= 500 && dayOf(ev.ts) === dayKey) out.push(ev); } catch (e) {}
|
||
return out;
|
||
}
|
||
function paidOn(dayKey) {
|
||
let wei = 0n, n = 0;
|
||
try { for (const ev of X.chain.recentEvents(1e9)) if ((ev.type === 'TierPaid' || ev.type === 'AwardPaid') && dayOf(ev.ts) === dayKey) { wei += BigInt(ev.amountWei); n++; } } catch (e) {}
|
||
return { pol: Number(wei / 10n ** 14n) / 10000, n };
|
||
}
|
||
function stats(dayKey) {
|
||
const day = dayKey || dayOf(Date.now());
|
||
const p = purchasesOn(day); const paid = paidOn(day);
|
||
const usd = p.reduce((s, e) => s + e.priceCents, 0) / 100;
|
||
const buyers = new Set(p.map(e => e.buyerId)).size;
|
||
const bonus = Object.values(S.granted || {}).filter(g => g.day === day).reduce((s, g) => s + g.bonus, 0);
|
||
return { day, packs: p.length, usd, buyers, polPaid: paid.pol, payouts: paid.n, bonusCredits: bonus };
|
||
}
|
||
// public view
|
||
function view() {
|
||
const c = cfg(); const now = Date.now(); const live = liveNow(now); const next = nextFriday(now);
|
||
return { on: c.on, pct: c.pct, live, today: live ? stats(dayOf(now)) : null, nextFriday: next, nextLabel: next ? labelOf(next) : null, start: c.start };
|
||
}
|
||
|
||
// the bonus: once per purchase tx, on a Friday on or after the start
|
||
async function onEvent(ev) {
|
||
if (!ev || ev.type !== 'Purchase' || !liveNow(ev.ts) || ev.priceCents < 500) return null;
|
||
if (S.granted[ev.tx]) return null;
|
||
const c = cfg(); const bonus = Math.round((Number(ev.creditAmount) || 0) * c.pct / 100);
|
||
if (bonus <= 0) return null;
|
||
const a = await X.accounts.byMemberId(ev.buyerId); if (!a || !a.email) return null;
|
||
const usd = ('$' + (ev.priceCents / 100).toFixed(2)).replace(/\.00$/, '');
|
||
await X.ads.addEarned(a.email, bonus, { purchased: true, log: { kind: 'bonus', note: 'Five Dollar Friday: +' + c.pct + '% on your ' + usd + ' package', ref: 'fdf:' + String(ev.tx).slice(0, 30) } });
|
||
S.granted[ev.tx] = { day: dayOf(ev.ts), memberId: ev.buyerId, bonus, at: Date.now() }; save();
|
||
try { await X.notify(ev.buyerId, 'Five Dollar Friday: +' + bonus + ' bonus credits', 'Your ' + usd + ' package landed on a Five Dollar Friday, so ' + bonus + ' bonus credits (+' + c.pct + '%) were added to your balance. Every pack bought on a Friday gets this. See your balance: https://instantadpay.com/my#campaigns', 'notice'); } catch (e) {}
|
||
// the wave, every ten packs
|
||
const st = stats(dayOf(ev.ts));
|
||
if (st.packs > 0 && st.packs % 10 === 0 && !S.posts[st.day + ':' + st.packs]) {
|
||
S.posts[st.day + ':' + st.packs] = Date.now(); save();
|
||
X.telegram('\u{1F4B5} <b>Five Dollar Friday</b> · <b>' + st.packs + ' packs</b> so far today from ' + st.buyers + ' buyers, <b>' + st.polPaid.toFixed(2) + ' POL</b> paid to members in the same transactions.\nEvery $5+ pack today earns +' + c.pct + '% credits: https://instantadpay.com/my#buy').catch(() => {});
|
||
}
|
||
return bonus;
|
||
}
|
||
|
||
// five Fridays in a row (the most recent five promo Fridays that have happened), any $5+ pack each
|
||
function fridayStreak(memberId) {
|
||
const c = cfg(); const now = Date.now(); const days = [];
|
||
for (let d = 0; d < 7 * 8 && days.length < 5; d++) { const ts = now - d * 86400000; const k = dayOf(ts); if (isFriday(ts) && k >= c.start && (k < dayOf(now) || liveNow(now))) days.push(k); }
|
||
let streak = 0;
|
||
for (const k of days) { if (purchasesOn(k).some(e => e.buyerId === Number(memberId))) streak++; else break; }
|
||
return streak;
|
||
}
|
||
function earnedBadge(memberId) {
|
||
if (S.badges[memberId]) return true;
|
||
if (fridayStreak(memberId) >= 5) { S.badges[memberId] = Date.now(); save(); return true; }
|
||
return false;
|
||
}
|
||
|
||
// scheduled moments, checked every few minutes: Thursday 18:00 CT notice + email, Friday 09:00 CT kickoff,
|
||
// Saturday 00:xx CT wrap-up of the Friday that just ended
|
||
async function tick() {
|
||
const c = cfg(); if (!c.on) return;
|
||
const now = Date.now(); const day = dayOf(now); const wd = weekdayOf(now); const h = hourOf(now);
|
||
const next = nextFriday(now);
|
||
if (wd === 'Thu' && h >= 18 && next && !S.sent['thu:' + next]) {
|
||
S.sent['thu:' + next] = now; save();
|
||
const subject = 'Tomorrow is Five Dollar Friday';
|
||
const text = 'Tomorrow is Five Dollar Friday on InstantAdPay.\n\nAny ad package of $5 or more bought on Friday (Central time) earns +' + c.pct + '% bonus credits, added the moment the purchase settles. Your $5 pays your sponsor line the second it clears, and your line’s $5s pay you. Friday is when we all push at once.\n\nLine up your pack: https://instantadpay.com/my#buy\nWatch the wave on the live ledger: https://instantadpay.com/ledger';
|
||
await broadcast(subject, text);
|
||
}
|
||
if (wd === 'Fri' && h >= 9 && day >= c.start && !S.sent['kick:' + day]) {
|
||
S.sent['kick:' + day] = now; save();
|
||
X.telegram('\u{1F4B5} <b>It’s Five Dollar Friday.</b> Any ad package from $5 up earns <b>+' + c.pct + '% credits</b> today, and every pack pays its sponsor line in the same transaction. Buy yours, then watch the ledger fill up: https://instantadpay.com/my#buy').catch(() => {});
|
||
}
|
||
if (wd === 'Sat' && h < 3) {
|
||
const fri = dayOf(now - 86400000);
|
||
if (isFriday(now - 86400000) && fri >= c.start && !S.sent['wrap:' + fri]) {
|
||
S.sent['wrap:' + fri] = now; save();
|
||
const st = stats(fri); const top = await topLines(fri);
|
||
X.telegram('\u{1F3C1} <b>Five Dollar Friday wrap-up</b> (' + labelOf(fri) + ')\n<b>' + st.packs + ' packs</b> from ' + st.buyers + ' buyers · $' + st.usd.toFixed(0) + ' in ad packages · <b>' + st.polPaid.toFixed(2) + ' POL</b> paid to members · ' + st.bonusCredits.toLocaleString() + ' bonus credits handed out'
|
||
+ (top.length ? '\nBiggest lines today: ' + top.map(t => t.who + ' (' + t.packs + ')').join(', ') : '') + '\nNext one is a week away. Line it up: https://instantadpay.com/my#buy').catch(() => {});
|
||
}
|
||
}
|
||
}
|
||
// most packs bought by a sponsor's directs on a day (the line that pushed hardest)
|
||
async function topLines(dayKey) {
|
||
const by = new Map();
|
||
for (const ev of purchasesOn(dayKey)) { let sp = 0; try { const m = await X.chain.member(ev.buyerId); sp = m && m.sponsorId ? Number(m.sponsorId) : 0; } catch (e) {} if (!sp) continue; by.set(sp, (by.get(sp) || 0) + 1); }
|
||
const rows = [...by.entries()].sort((a, b) => b[1] - a[1]).slice(0, 3);
|
||
const out = []; for (const [id, packs] of rows) { let who = '#' + id; try { const a = await X.accounts.byMemberId(id); if (a && a.username) who = '@' + a.username; } catch (e) {} out.push({ memberId: id, who, packs }); }
|
||
return out;
|
||
}
|
||
// on-site notice + email to every active member (quiet 45+ days skipped, drip unsubscribes honoured)
|
||
async function broadcast(subject, text) {
|
||
let all = []; try { all = await X.accounts.listAll(5000); } catch (e) { return; }
|
||
const now = Date.now(); let n = 0;
|
||
for (const a of all) {
|
||
if (!a.email) continue;
|
||
if (now - (a.lastSeen || a.created || 0) > 45 * 86400000) continue;
|
||
try { await X.notify(a.memberId, subject, text, 'notice'); } catch (e) {}
|
||
try { if (X.mailer && X.mailer.hasKey() && !(X.drip && await X.drip.isUnsubscribed(a.email))) { await X.mailer.send(a.email, subject, text + '\n\nInstantAdPay'); n++; } } catch (e) {}
|
||
}
|
||
return n;
|
||
}
|
||
|
||
module.exports = { init, cfg, view, stats, onEvent, tick, fridayStreak, earnedBadge, liveNow, nextFriday, isFriday, labelOf, topLines };
|