Traffic Desk: stop-and-refund, plus never let a banner dead-end
Three fixes from live testing of the Traffic Desk: - /p/<id> with no page built now 302s to /join/<id> instead of returning raw JSON 404. Any /p/ link already on a banner, flyer, or in a DM must always land somewhere useful. - The personal-page ad destination is only offered (client) and only accepted (server) once a page actually exists; otherwise the member is pointed at the Page Builder. - Members can stop a running banner and get the unserved impressions back in their monthly balance. Counters are read before deactivation, since deactivating zeroes `remaining` and would look fully served. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
+45
-4
@@ -122,9 +122,14 @@ async function launch(opts) {
|
||||
throw new Error('That is more than your remaining ' + st.remaining.toLocaleString() + ' impressions this month.');
|
||||
}
|
||||
|
||||
const target = opts.target === 'page'
|
||||
? 'https://rmcircle.team/p/' + opts.id
|
||||
: 'https://rmcircle.team/join/' + opts.id + (opts.angle ? '?v=' + opts.angle : '');
|
||||
// Server-side guard: only allow the personal page as a destination when one
|
||||
// actually exists. /p/<id> also redirects to /join/<id> when empty, so a live
|
||||
// banner can never dead-end — but we shouldn't create that situation at all.
|
||||
let target = 'https://rmcircle.team/join/' + opts.id + (opts.angle ? '?v=' + opts.angle : '');
|
||||
if (opts.target === 'page') {
|
||||
if (!opts.hasPage) throw new Error('Build your personal page first, then you can point ads at it.');
|
||||
target = 'https://rmcircle.team/p/' + opts.id;
|
||||
}
|
||||
|
||||
const idem = 'rmc-' + opts.id + '-' + monthKey() + '-' + crypto.randomBytes(6).toString('hex');
|
||||
const res = await callNas({
|
||||
@@ -144,10 +149,46 @@ async function launch(opts) {
|
||||
return entry;
|
||||
}
|
||||
|
||||
// Stop a running banner and return the UNSERVED impressions to the member's
|
||||
// monthly balance. Order matters: read the counters BEFORE deactivating,
|
||||
// because deactivation zeroes `remaining` and would make it look fully served.
|
||||
async function stop(memberId, adId) {
|
||||
const all = readLedger();
|
||||
const mk = monthKey();
|
||||
const rows = ((all[mk] || {})[String(memberId)]) || [];
|
||||
const row = rows.find(function (r) { return Number(r.adId) === Number(adId); });
|
||||
if (!row) throw new Error('That banner is not one of yours from this month.');
|
||||
if (row.stopped) throw new Error('That banner is already stopped.');
|
||||
|
||||
let served = 0, unserved = 0;
|
||||
try {
|
||||
const s = await callNas({ action: 'stats', ad_ids: [Number(adId)] });
|
||||
const st = (s.stats || [])[0];
|
||||
if (st) {
|
||||
served = Math.max(0, Number(st.served) || 0);
|
||||
unserved = Math.max(0, Number(st.remaining) || 0);
|
||||
}
|
||||
} catch (e) { /* if stats are unavailable, refund nothing rather than guess */ }
|
||||
|
||||
await callNas({ action: 'deactivate', ad_id: Number(adId) });
|
||||
|
||||
// Charge only what actually served; the rest returns to the allowance.
|
||||
// `bought` preserves the original order size so the member's history still
|
||||
// shows what they launched, not just what it ended up costing them.
|
||||
if (row.bought == null) row.bought = row.impressions;
|
||||
row.stopped = true;
|
||||
row.stoppedAt = new Date().toISOString();
|
||||
row.served = served;
|
||||
row.refunded = unserved;
|
||||
row.impressions = served; // what this campaign counts against the month
|
||||
writeLedger(all);
|
||||
return { adId: Number(adId), served: served, refunded: unserved };
|
||||
}
|
||||
|
||||
async function stats(adIds) {
|
||||
if (!adIds || !adIds.length) return [];
|
||||
const r = await callNas({ action: 'stats', ad_ids: adIds.slice(0, 200) });
|
||||
return r.stats || [];
|
||||
}
|
||||
|
||||
module.exports = { init, configured, status, launch, stats, allowanceFor, sizes, CREATIVES, ALLOWANCE };
|
||||
module.exports = { init, configured, status, launch, stop, stats, allowanceFor, sizes, CREATIVES, ALLOWANCE };
|
||||
|
||||
Reference in New Issue
Block a user