# Ads Platform — Technical Specification (Draft)

Unified **module** implementation — one `api/ads.php`, one hub UI, `user_id` everywhere.

---

## Database (migration `031-ads-platform.sql`)

### Extend subscription plan type

```sql
ALTER TABLE subscription_plans
  MODIFY plan_type ENUM('general','sure_odds','casino','forex','ads') NOT NULL DEFAULT 'general';
```

### Tables

```sql
-- Business profile on existing user (NOT separate login)
CREATE TABLE ad_profiles (
  id INT AUTO_INCREMENT PRIMARY KEY,
  user_id INT NOT NULL UNIQUE,
  account_type ENUM('individual','company') NOT NULL DEFAULT 'individual',
  display_name VARCHAR(160) NOT NULL,
  company_name VARCHAR(160) NULL,
  company_reg_no VARCHAR(80) NULL,
  category VARCHAR(60) NULL,
  status ENUM('pending','approved','rejected','suspended') NOT NULL DEFAULT 'pending',
  rejection_reason VARCHAR(255) NULL,
  reviewed_by INT NULL,
  reviewed_at DATETIME NULL,
  created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
  FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,
  INDEX idx_status (status)
);

-- Campaigns owned by user_id
CREATE TABLE ad_campaigns (
  id INT AUTO_INCREMENT PRIMARY KEY,
  user_id INT NOT NULL,
  title VARCHAR(160) NOT NULL,
  description TEXT NULL,
  ad_type ENUM('image','video') NOT NULL,
  media_url VARCHAR(500) NOT NULL,
  media_thumb_url VARCHAR(500) NULL,
  target_url VARCHAR(500) NULL,
  cta_label VARCHAR(80) DEFAULT 'Learn more',
  budget_total DECIMAL(15,2) NOT NULL,
  budget_spent DECIMAL(15,2) NOT NULL DEFAULT 0,
  cost_per_view DECIMAL(15,2) NOT NULL,
  viewer_reward DECIMAL(15,2) NOT NULL,
  platform_fee DECIMAL(15,2) NOT NULL DEFAULT 0,
  min_watch_seconds INT NOT NULL DEFAULT 15,
  completions_count INT NOT NULL DEFAULT 0,
  starts_at DATETIME NULL,
  ends_at DATETIME NULL,
  status ENUM('draft','pending','active','paused','ended','rejected') NOT NULL DEFAULT 'draft',
  rejection_reason VARCHAR(255) NULL,
  reviewed_by INT NULL,
  reviewed_at DATETIME NULL,
  created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
  FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,
  INDEX idx_status_dates (status, starts_at, ends_at),
  INDEX idx_owner (user_id, status)
);

-- Watch sessions (earners)
CREATE TABLE ad_views (
  id BIGINT AUTO_INCREMENT PRIMARY KEY,
  campaign_id INT NOT NULL,
  user_id INT NOT NULL,
  status ENUM('started','completed','rejected') NOT NULL DEFAULT 'started',
  watch_seconds INT NOT NULL DEFAULT 0,
  reward_paid DECIMAL(15,2) NOT NULL DEFAULT 0,
  started_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
  completed_at DATETIME NULL,
  FOREIGN KEY (campaign_id) REFERENCES ad_campaigns(id) ON DELETE CASCADE,
  FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,
  INDEX idx_user_day (user_id, completed_at),
  INDEX idx_campaign (campaign_id, status)
);
```

### Module + plans seed

```sql
INSERT IGNORE INTO modules (slug, name, icon, description, status, sort_order) VALUES
('ads', 'Ads', '📺', 'Watch ads to earn or post your business ads', 'enabled', 45);

INSERT IGNORE INTO subscription_plans (plan_code, name, plan_type, price, duration_days, status) VALUES
('ADS-WEEK', 'Ads Weekly', 'ads', 5000, 7, 'active'),
('ADS-MONTH', 'Ads Monthly', 'ads', 15000, 30, 'active');
```

Link `unlock_rules` for module `ads` → subscription plan(s).

---

## API — `api/ads.php` (single endpoint file)

### Watch & earn (requires Ads subscription)

| Action | Method | Description |
|--------|--------|-------------|
| `feed` | GET | Active ads user can complete today |
| `detail` | GET | Single campaign media |
| `start` | POST | Begin view session → `viewId` |
| `complete` | POST | Validate watch time → pay wallet |
| `my_earnings` | GET | Earn history / today total |

Guards: `requireAuth('user')` + `requireModuleAccess($db, $uid, 'ads')` + not own campaign.

### Post ads (requires approved profile)

| Action | Method | Description |
|--------|--------|-------------|
| `profile` | GET | Current user's ad profile |
| `profile_save` | POST | Register / update business profile → pending |
| `my_campaigns` | GET | User's campaigns |
| `campaign_create` | POST | Upload + budget (debit balance) |
| `campaign_submit` | POST | draft → pending review |

Guards: `requireAuth('user')` + `requireApprovedAdProfile()` for create/submit.

### Admin (`admin_extensions.php`)

| Action | Description |
|--------|-------------|
| `ads_dashboard` | KPIs |
| `ads_profiles_list` / `ads_profile_approve` / `_reject` | |
| `ads_campaigns_list` / `ads_campaign_approve` / `_reject` / `_pause` | |
| `ads_settings_get` / `_save` | |

---

## Service layer (`config/ads_service.php`)

```php
function requireApprovedAdProfile(PDO $db, int $userId): array
function completeAdView(PDO $db, int $viewerId, int $viewId, int $watchSeconds): array
function fundCampaignFromBalance(PDO $db, int $userId, int $campaignId, float $amount): void
```

`completeAdView`:

1. Transaction + row locks  
2. Reject if viewer = campaign owner  
3. Subscription + min watch + daily cap  
4. `creditWallet()` viewer, increment `budget_spent`  
5. Pause if budget exhausted  
6. `notify()`  

---

## Frontend (`assets/ads/ads-hub.js`)

One component, tabbed UI inside `#u-ads`:

```text
AdsHub.render('u-ads')
  ├── tab: watch    → AdsHub.renderWatch()
  ├── tab: post     → AdsHub.renderPost()
  ├── tab: campaigns → AdsHub.renderMyCampaigns()
  └── tab: earnings → AdsHub.renderEarnings()
```

Hook in `hub-ui.js`:

```javascript
async function rUAds() {
  await AdsHub.render('u-ads');
}
```

---

## Implementation order

1. Migration + module + Ads subscription plans + unlock rule  
2. `api/ads.php` watch flow (image) + `ads-hub.js` Watch tab  
3. Ad profile + campaign create (Post tab) + balance debit  
4. Admin approve profiles & campaigns  
5. Video ads + My Campaigns / My Earnings tabs  
6. Engagement (missions, leaderboard)  

---

## Testing checklist

- [ ] One user can subscribe, watch, earn — same session  
- [ ] Same user can register profile, post ad — no second login  
- [ ] User cannot earn on own campaign  
- [ ] No subscription → 403 on `feed` / `complete`  
- [ ] No approved profile → 403 on `campaign_create`  
- [ ] Insufficient balance → cannot fund campaign  
