Refill conversions: only count a genuine response. An unrelated ad already running is not a relaunch, and a still-running low campaign is not a conversion at all; adds a reset lever to re-decide verdicts
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -180,9 +180,17 @@ async function checkConversions() {
|
|||||||
try { const ps = await X.accounts.positions(r.owner); ids = [...new Set([...ids, ...ps.map(p => p.memberId).filter(Boolean)])]; } catch (e) {}
|
try { const ps = await X.accounts.positions(r.owner); ids = [...new Set([...ids, ...ps.map(p => p.memberId).filter(Boolean)])]; } catch (e) {}
|
||||||
const buy = evs.find(e => ids.includes(Number(e.id || e.buyerId)) && norm(e) > since);
|
const buy = evs.find(e => ids.includes(Number(e.id || e.buyerId)) && norm(e) > since);
|
||||||
if (buy) { mark(r, 'purchase', { cents: n0(buy.priceCents), tx: buy.tx || null }); continue; }
|
if (buy) { mark(r, 'purchase', { cents: n0(buy.priceCents), tx: buy.tx || null }); continue; }
|
||||||
// 2. put an ad back up, or topped one up
|
// 2. put an ad back up. Only two things count: a campaign created after we wrote to them, or one of
|
||||||
const fresh = all.find(c => c.owner === r.owner && !c.house && (n0(c.created) > since || (c.status === 'active' && !r.campaigns.some(x => x.id === c.id))));
|
// the very campaigns we named coming back to life. An unrelated ad that was already running is not a
|
||||||
|
// response to the email, and counting it was inflating the rate to meaninglessness.
|
||||||
|
const fresh = all.find(c => c.owner === r.owner && !c.house && n0(c.created) > since);
|
||||||
if (fresh) { mark(r, 'relaunch', { campaignId: fresh.id, name: fresh.name || null }); continue; }
|
if (fresh) { mark(r, 'relaunch', { campaignId: fresh.id, name: fresh.name || null }); continue; }
|
||||||
|
// only meaningful for an ad that had actually stopped: a "running low" ad is still active by
|
||||||
|
// definition, so counting that as a response would mark every low notice converted on the spot
|
||||||
|
if (r.state === 'out') {
|
||||||
|
const revived = all.find(c => c.status === 'active' && r.campaigns.some(x => x.id === c.id) && delivered(c) < n0(c.budget));
|
||||||
|
if (revived) { mark(r, 'resumed', { campaignId: revived.id, name: revived.name || null }); continue; }
|
||||||
|
}
|
||||||
const topped = all.find(c => { const was = r.campaigns.find(x => x.id === c.id); return was && n0(c.budget) > was.budget; });
|
const topped = all.find(c => { const was = r.campaigns.find(x => x.id === c.id); return was && n0(c.budget) > was.budget; });
|
||||||
if (topped) { mark(r, 'topup', { campaignId: topped.id, added: Math.round(n0(topped.budget) - r.campaigns.find(x => x.id === topped.id).budget) }); }
|
if (topped) { mark(r, 'topup', { campaignId: topped.id, added: Math.round(n0(topped.budget) - r.campaigns.find(x => x.id === topped.id).budget) }); }
|
||||||
}
|
}
|
||||||
@@ -222,4 +230,15 @@ function stats() {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = { init, tick, stats, PACKS };
|
// clears conversion verdicts so the corrected rule can re-decide them (used once after the
|
||||||
|
// first run, where "already had another ad running" was wrongly counted as a relaunch)
|
||||||
|
function resetConversions(kinds) {
|
||||||
|
S.records = S.records || {};
|
||||||
|
const want = new Set(kinds && kinds.length ? kinds : ['relaunch']);
|
||||||
|
let n = 0;
|
||||||
|
for (const r of Object.values(S.records)) if (r.converted && want.has(r.converted.kind)) { r.converted = null; n++; }
|
||||||
|
save();
|
||||||
|
return { cleared: n };
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = { init, tick, stats, resetConversions, PACKS };
|
||||||
|
|||||||
@@ -2912,6 +2912,11 @@ const server = http.createServer(async (req, res) => {
|
|||||||
if (!isAdmin(req)) return json(res, 401, { error: 'auth' });
|
if (!isAdmin(req)) return json(res, 401, { error: 'auth' });
|
||||||
return json(res, 200, refill.stats());
|
return json(res, 200, refill.stats());
|
||||||
}
|
}
|
||||||
|
if (p === '/api/admin/refill/reset' && req.method === 'POST') { // re-decide conversions under a corrected rule
|
||||||
|
if (!isAdmin(req)) return json(res, 401, { error: 'auth' });
|
||||||
|
const b = await readBody(req);
|
||||||
|
return json(res, 200, refill.resetConversions(Array.isArray(b.kinds) ? b.kinds : null));
|
||||||
|
}
|
||||||
if (p === '/api/admin/refill/run' && req.method === 'POST') { // send the next batch now instead of waiting for the tick
|
if (p === '/api/admin/refill/run' && req.method === 'POST') { // send the next batch now instead of waiting for the tick
|
||||||
if (!isAdmin(req)) return json(res, 401, { error: 'auth' });
|
if (!isAdmin(req)) return json(res, 401, { error: 'auth' });
|
||||||
return json(res, 200, await refill.tick());
|
return json(res, 200, await refill.tick());
|
||||||
|
|||||||
Reference in New Issue
Block a user