# Advertising and challenge boosts

The Advertising & boosts page supports individual and organization account holders. Purchases are separate from organizer subscriptions and never change credibility, verification or leaderboard results.

| Placement | Initial daily price | Where it appears |
| --- | --- | --- |
| Challenge boost | ₦500 | Sponsored cards on Challenges and Explore; category-filtered challenge lists |
| Category advertisement | ₦1,000 | The selected category's challenge list and challenge detail pages |
| Homepage advertisement | ₦2,000 | Public landing page |

The model sells rotating placement time, not exclusive display or guaranteed reach. One sponsored card is selected at random for each available placement on a page load. There are up to five concurrent campaigns in each placement/category booking pool. Category boosts may also rotate in the general challenge/explore pool. Rate changes affect new submissions; existing quotes and paid bookings retain their rate.

## Advertiser workflow

1. Verify your account email, then open Advertising & boosts or use Boost this challenge on your own published challenge.
2. Select a placement, any positive whole number of days (including 365, 730 or more), and an optional future preferred start. There is no 30-day product limit. Calendar storage ends at year 9999 and checkout totals must fit the payment ledger; oversized values are rejected before saving. General ads require a headline, description, HTTP/HTTPS destination and JPG/PNG/WebP image up to 2 MB. Images are re-encoded to a 1200×675 canvas without cropping. Boosts use the challenge's existing title and question.
3. Save and pay the displayed total using Paystack. Unpaid campaigns remain outside the admin review queue and do not send review notifications. The campaign content and quoted rate are fixed for the submission. Availability is checked before checkout. Checkout holds a rotating slot for 15 minutes; repeated checkout during that window resumes the same purchase.
4. Verified payment submits the campaign for independent admin review and notifies administrators. Payment alone never publishes an advertisement. Availability is checked again at approval; payment does not guarantee approval or reserve a slot throughout review.
5. After approval, the campaign starts at the later of approval or the preferred start. Each purchased day runs for 24 hours; waiting for review does not consume paid days. Boosts must fit entirely before the challenge's entry deadline, including after any review delay. All of your published challenges appear in the dropdown without the former 100-record cutoff; those with less than one day remaining are visibly disabled with an explanation. Scheduled campaigns only appear after their start; expired, stopped and unavailable challenges do not serve ads.

The dashboard shows status, running dates, daily price, total price, feedback, metrics and a printable payment receipt. In development without a Paystack key, an explicitly labelled test checkout simulates payment without charging money.

## Review, failed delivery and refunds

Admins/super admins review images and destinations, approve/reject, change rates, stop campaigns and inspect audit records. An admin cannot approve their own advertisement. Notifications are queued for reviews and payment outcomes.

Rejected paid campaigns are marked Refund required. The same applies if payment arrives too late or approval cannot deliver the full booked duration (for example, a challenge closes too soon). The payment remains successful until an external refund is recorded. Campaigns are never silently shortened. The advertiser receives the review reason by notification/email. A paid superseded checkout is recorded as a duplicate payment requiring a refund and does not extend delivery.

Refunds are performed through the Paystack dashboard. Admins then record the external refund reference and amount in TimeProof. The record-refund controls do not transfer money. Duplicate payments have their own refund controls so the valid campaign can continue. Publish your refund and advertising-content policies before accepting real customers. Stopping a paid campaign is an admin decision; it does not automatically send a refund.

## Measurement

An impression is requested only after at least half the sponsored card has been visible for one second in an active browser tab. The server validates a session-bound token and checks the campaign is live. Impressions and clicks are deduplicated per campaign, browser session and app-timezone day. Advertisers' own sessions and logged-in staff are excluded. No raw IP address is stored in advertising events.

Challenge entry attribution uses the most recent boosted-challenge click in the same session within 24 hours. Only an actual saved entry can be attributed, and an entry is counted once. These are estimates, not independently audited people/reach or sales measurements; browsers that block tracking or start new sessions affect counts.

## Deployment

Run `php database/migrate_advertising.php` on an existing installation. This adds tables, widens duration/total storage, and moves legacy unpaid pending/approved campaigns to Awaiting payment. Existing paid running campaigns and configured rates are preserved. The migration is safe to repeat. Fresh imports already include advertising. Clean production exports contain rates only, with no advertisers, campaigns, payments or tracking activity.

Run `php cron/advertising.php` every minute for status maintenance. Delivery also checks current dates on every request, so an expired campaign is not served while waiting for the job. Configure SMTP/email jobs and Paystack as documented in CONFIGURATION.md. Advertising uses the existing signed payment webhook; no additional Paystack webhook is needed.

Uploaded ad images are inside the encrypted backup media root. Keep its existing HTTP access restrictions. Serve only normalized images through the authorized ad-image endpoint.

Validation: `php -d xdebug.mode=off tests/integration.php`, `node tests/advertising_frontend.cjs`, and `php -d xdebug.mode=off tests/clean_production_database.php`.
