Commit Graph

5 Commits

Author SHA1 Message Date
martbost cb6da01e1c Stop billing members for syndicated impressions that were never delivered
Network Ad Space has no stop flag. Its serving query only picks rows with remaining>0,
so zeroing that counter is the only way to halt an ad. But delivery is derived as
assigned - remaining, so the moment an ad was stopped it read back as 100% DELIVERED.

That was not only a reporting error. reconcileNas() runs every five minutes, had no
status filter, and CHARGES member credits off that figure. So pausing a campaign, or
ending one, made the next reconcile pass bill the member for the entire unspent budget
as though it had all been served.

Measured on production before the fix: 23 member campaigns across 16 members, every
single one charged to exactly 100% of budget, 11,437 credits in total, against on-site
delivery evidence of roughly 4,700 impressions. A naturally exhausted ad also ends at
remaining=0, so the two cases cannot be told apart after the fact, which is why the
true figure has to be captured before the stop.

Three changes:
- nas.deactivate() now reads the real served count BEFORE zeroing and returns it.
- a new ads.stopNas() helper is the only path to a stop, and it persists that figure as
  the campaign's final delivery. No caller touches nas.deactivate() directly any more.
- reconcileNas() only processes campaigns with status='active'. A stopped ad delivers
  nothing further, so there is never anything legitimate left to charge for.

qa/nas-served.mjs (8 assertions) stubs the NAS layer and drives the real code: pausing
records the true 3,000 rather than the 10,000 allocation, a paused campaign is never
charged afterwards and its figure never jumps to the allocation, and an active campaign
still reconciles and is charged normally so the guard did not break delivery.

fraud-allow 12, sponsor-note 5, chatbot-parse 35, qa/run.sh member 0 bugs.

Historical delivery is not recoverable: the stop overwrote the only record of it.
Refunding the 11,437 credits to the 16 affected members is Marty's call, pending.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-17 19:52:12 -05:00
martbost 82269eccc8 NAS: syndicated ads click through our redirect so network clicks count and carry the source site
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-10 10:16:22 -05:00
martbost 6e187c4f41 Campaigns: optional daily cap on banner/text paces spend on-site and hands Network Ad Space one day's allowance at a time
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-10 10:03:57 -05:00
martbost 289538dbeb NAS: adtype is an integer column (1), not a string — fixes sponsorads insert
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-09-06 08:21:59 -05:00
martbost 57bba0111a NAS syndication adapter (feature-flagged off): push banner/text to Network Ad Space, reconcile delivery into unified credit pool
- nas.js: direct-MySQL writer mirroring CTB's proven payments+sponsorads path (approved:1, pid 1/2, catid 5, remaining counts DOWN)
- Create-time push for banner/text; pause/resume mirrors to NAS deactivate/topUp
- reconcileNas() 5-min loop: reads served, charges delta credits to the shared budget, stops NAS ad when budget dry
- Campaign row shows IAP + '+N net' network views (differentiated stats)
- Inert unless NAS_DB_* env is set; zero prod impact until enabled
- Also: rename Inbox sub-tab to 'Inbox Ads'

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-09-06 08:11:32 -05:00