Partner promo codes: reusable codes that add free ad credits, tracked per redemption
promos.js (promo_codes + promo_redemptions, JSON fallback): create/update codes with credits, partner, optional cap and expiry, on/off. Redeemed once per account either from a join link ?promo=CODE (cookie, applied at signup) or the Overview box 'Have a promo code?' (/api/my/promo/redeem). Admin > Traffic gets a Partner promo codes card (create, toggle, uses, recent redemptions). Chatbot line added. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
@@ -27,6 +27,7 @@ const coach = require('./coach'); // coaching view, nudges, digest, prospects,
|
||||
const tank = require('./tank'); // holding tank: unsponsored free members, adoptions, pay-it-forward
|
||||
const legacy = require('./legacy'); // Faucet Wave / Tier One Ads bridge: welcome-back credits for listed emails
|
||||
const traffic = require('./traffic'); // public page views by referring domain (admin Traffic tab)
|
||||
const promos = require('./promos'); // partner promo codes -> free ad credits (link ?promo=CODE or the dashboard box)
|
||||
const TRAFFIC_PAGES = new Set(['/', '/ledger', '/contract', '/shorts', '/plays', '/wallets', '/launch']);
|
||||
let tankWaitCache = null; // dashboard: who is waiting for a sponsor (refreshed every minute)
|
||||
const geo = require('./geo'); // viewer country -> tier (DB-IP lite), for campaign targeting
|
||||
@@ -322,6 +323,7 @@ async function boot() {
|
||||
tank.init({ dataDir: DATA_DIR, chain, accounts, messages, mailer, site: 'https://instantadpay.com' });
|
||||
legacy.init({ dataDir: DATA_DIR });
|
||||
traffic.init({ dataDir: DATA_DIR });
|
||||
promos.init({ dataDir: DATA_DIR });
|
||||
setInterval(() => tankNotifyTick().catch(e => console.error('tank notify', e.message)), 15 * 60 * 1000); // new tank arrivals -> Telegram
|
||||
geo.init({ dataDir: DATA_DIR }).catch(e => console.error('geo init', e.message));
|
||||
setInterval(() => geo.refresh().catch(e => console.error('geo refresh', e.message)), 24 * 3600 * 1000); // monthly file, checked daily
|
||||
@@ -635,6 +637,7 @@ const server = http.createServer(async (req, res) => {
|
||||
const cookieTail = `; Path=/; SameSite=Lax; Max-Age=${30 * 24 * 3600}${IS_PROD ? '; Secure' : ''}`; // 30 days: whoever brings them back gets the credit
|
||||
const set = [];
|
||||
set.push('iap.sponsor=' + tok + cookieTail); // last touch wins
|
||||
const promo = promos.norm(u.searchParams.get('promo')); if (promo) set.push('iap.promo=' + promo + cookieTail); // partner code, redeemed at signup
|
||||
if (ang) set.push('iap.angle=' + angle + cookieTail);
|
||||
if (!cookies['iap.ref']) set.push('iap.ref=' + encodeURIComponent(coach.refHost(req.headers.referer)) + cookieTail); // first-touch source
|
||||
return serveJoinPage(res, tok, ang ? angle : '', ang, set);
|
||||
@@ -839,6 +842,11 @@ const server = http.createServer(async (req, res) => {
|
||||
await auth.logout(req);
|
||||
}
|
||||
if (r.created) { sendWelcome(e, ref).catch(() => {}); } // sponsor notified at username set (/api/my/profile)
|
||||
// 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) { await ads.addEarned(e, g.credits); 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); } }
|
||||
@@ -1070,6 +1078,33 @@ const server = http.createServer(async (req, res) => {
|
||||
}
|
||||
// -- coaching: every direct's ladder rung, stalled flag, and what to say
|
||||
// -- holding tank: waiting members, my adoptions, adopt, release (pay it forward)
|
||||
// -- promo code typed on the dashboard
|
||||
if (p === '/api/my/promo/redeem' && req.method === 'POST') {
|
||||
const s = await auth.fromRequest(req);
|
||||
if (!s || !s.email) return json(res, 401, { error: 'Sign in first.' });
|
||||
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);
|
||||
return json(res, 200, { ok: true, credits: g.credits, code: g.code, partner: g.partner });
|
||||
}
|
||||
// -- admin: promo codes (create/update, switch on/off, redemptions)
|
||||
if (p === '/api/admin/promos' && req.method === 'GET') {
|
||||
if (!isAdmin(req)) return json(res, 401, { error: 'auth' });
|
||||
return json(res, 200, await promos.adminView());
|
||||
}
|
||||
if (p === '/api/admin/promos' && req.method === 'POST') {
|
||||
if (!isAdmin(req)) return json(res, 401, { error: 'auth' });
|
||||
const b = await readBody(req);
|
||||
const r = await promos.create(b);
|
||||
return json(res, r.error ? 400 : 200, r);
|
||||
}
|
||||
if (p === '/api/admin/promos' && req.method === 'PATCH') {
|
||||
if (!isAdmin(req)) return json(res, 401, { error: 'auth' });
|
||||
const b = await readBody(req);
|
||||
const r = await promos.setActive(b.code, !!b.active);
|
||||
return json(res, r.error ? 400 : 200, r);
|
||||
}
|
||||
if (p === '/api/my/tank' && 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