Files
instantadpay/friday.js
T

151 lines
9.8 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 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 };