Login ads become a flat 30-day slot
The daily meter charged 100 credits for every day a campaign was shown, with no end date. It produced 74-day queues, and worse, each new campaign added 100/day of charges while adding no inventory: on 18 September, 15 campaigns were charged 1,500 credits against a platform that had produced 367 login impressions in its entire life. That had stopped being a pricing inefficiency and become members paying for delivery that did not exist. Now: 3,000 credits buys 30 days of unlimited impressions, charged once at purchase, with no delivery promise — the model Marty wanted, matching how ClickBaitPays sells theirs. The price is deliberately identical to 30 days at the old daily rate, so nobody pays more than before; they just get a known end instead of an open-ended drain. It reuses the `featured` shape, which was already a flat up-front buy that runs to `expires`. Both daily meters — MySQL and JSON — now skip any campaign with an expiry, so a slot can never be metered. The existing sweep closes it on day 30 and returns anything unspent. qa/login-slot.mjs, 9 checks, including that the slot price still equals 30 days at the old rate and that neither meter can touch a slot campaign. Member walk clean. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,42 @@
|
||||
// Login ads are a flat 30-day slot, not a daily meter.
|
||||
//
|
||||
// The old model charged 100 credits for every day a campaign was shown, with no end date.
|
||||
// That produced 74-day queues, and every new campaign added 100/day of charges while adding
|
||||
// no inventory — on 18 Sep, 15 campaigns were charged 1,500 credits against a platform that
|
||||
// had produced 367 login impressions in its whole life.
|
||||
import { createRequire } from 'node:module';
|
||||
const require = createRequire(import.meta.url);
|
||||
const ads = require('../ads.js');
|
||||
|
||||
const ok = [], bad = [];
|
||||
const t = (n, c, extra) => { (c ? ok : bad).push(n + (c || !extra ? '' : ' -> ' + extra)); };
|
||||
|
||||
const r = ads.rates();
|
||||
t('a slot price is configured', Number(r.loginSlotCredits) > 0, String(r.loginSlotCredits));
|
||||
t('a slot length is configured', Number(r.loginSlotDays) > 0, String(r.loginSlotDays));
|
||||
t('the slot is 3,000 credits for 30 days', r.loginSlotCredits === 3000 && r.loginSlotDays === 30,
|
||||
r.loginSlotCredits + ' / ' + r.loginSlotDays);
|
||||
|
||||
// the price matches what 30 days used to cost, so nobody pays more than before
|
||||
t('same total as 30 days at the old daily rate',
|
||||
r.loginSlotCredits === 30 * r.loginCreditsPerDay,
|
||||
r.loginSlotCredits + ' vs ' + (30 * r.loginCreditsPerDay));
|
||||
|
||||
// the source guarantees that matter, checked directly — these are the behaviours that stop
|
||||
// a slot draining against days when the gate produced nothing
|
||||
const src = require('fs').readFileSync(new URL('../ads.js', import.meta.url), 'utf8');
|
||||
|
||||
t('buying a slot charges the whole price up front',
|
||||
/Login-ad slot \(/.test(src) && /chargeCredits\(campaign\.id, v\.c\.budget, 'Login-ad slot/.test(src));
|
||||
t('a slot gets an end date at purchase',
|
||||
/out\.expires = Date\.now\(\) \+ days \* 86400000/.test(src));
|
||||
t('the MySQL daily meter skips slot campaigns',
|
||||
/expires IS NULL AND \(last_day_charged/.test(src));
|
||||
t('the JSON daily meter skips slot campaigns',
|
||||
/if \(c\.expires\) continue; \/\/ slot buy/.test(src));
|
||||
t('a short budget is refused with the slot price named',
|
||||
/A login-ad slot is ' \+ cost \+ ' credits for ' \+ days \+ ' days/.test(src));
|
||||
|
||||
console.log('PASS ' + ok.length);
|
||||
for (const b of bad) console.log('FAIL ' + b);
|
||||
process.exit(bad.length ? 1 : 0);
|
||||
Reference in New Issue
Block a user