# Architecture - Ordio Loop Affiliate Partner System

**Last Updated:** 2026-03-25

## Code Organization & Modularization

The affiliate system has been optimized for maintainability and performance through modularization:

### Shared Components

**API Layer (`v2/helpers/`):**

- `affiliate-api-base.php` - Common API functions used by all endpoints:

  - `requireAffiliateAuthAPI()` - Authentication wrapper with error handling
  - `loadCachedHubSpotData()` - Cache loading with default structure support
  - `sendJSONResponse()` - Standardized JSON response formatting
  - `handleAPIError()` - Consistent error handling and logging
  - `getAuthenticatedPartnerValidated()` - Partner validation helper

- `affiliate-data-formatters.php` - Data formatting functions:
  - `formatLeadForAPI()` - Lead formatting with status determination
  - `formatDealForAPI()` - Deal formatting with MRR calculation
  - `calculateLeadCounts()` - Lead status counting
  - `formatMRRStatusBreakdown()` - MRR breakdown with percentages
  - `calculateConversionRates()` - Conversion rate calculations
  - `sortLeadsByDate()`, `sortDealsByDate()`, `sortDealsByMRR()` - Sorting utilities

**CSS Layer (`v2/css/`):**

Partner platform CSS uses dedicated files and **minified** assets in production. Load order and files:

| File                              | Loaded via                             | Contents                                                                                                                             |
| --------------------------------- | -------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------ |
| `affiliate-shared.min.css`        | `affiliate-head.php`                   | Design tokens, sidebar, cards, KPI, tables, buttons, empty/loading states, responsive (640px, 768px, 1024px), chart height utilities |
| `affiliate-dashboard.min.css`     | `affiliate-head.php`                   | Page-specific overrides for dashboard pages (minimal; base in shared)                                                                |
| `affiliate-levels.min.css`        | `partner-levels.php` customHead        | Levels page only: hero, stepper, badges, achievements                                                                                |
| `affiliate-referral-urls.min.css` | `partner-referral-urls.php` customHead | Referral URLs page: UTM form, sitemap table, presets, pagination                                                                     |
| `affiliate-admin.min.css`         | `partner-admin.php` customHead         | Admin (Verwaltung) page: flat cards, partner table with min-widths and horizontal scroll                                             |
| `affiliate-program-analytics.min.css` | `partner-program-analytics.php` customHead | Admin program analytics: filters, ops panel, chart slots, tables                                                                 |
| `affiliate-dialogs.min.css`       | partner-admin, referral-urls, settings | In-platform confirm and alert dialogs (replaces native alert/confirm)                                                                |

- **Production:** Always use `.min.css` with `filemtime(__DIR__ . '/../css/filename.min.css')` (or `file_exists` fallback) for cache busting.
- **After editing any `v2/css/affiliate-*.css`:** Run `npm run minify` to regenerate `.min.css` files.
- **No inline `<style>` blocks:** Page-specific styles live in dedicated CSS files (e.g. referral page styles in `affiliate-referral-urls.css`).

- `affiliate-shared.css` - Shared design tokens and common styles:
  - CSS custom properties (`:root` variables) for colors, spacing, layout
  - Sidebar styles (navigation, active states)
  - Common components (cards, buttons, tables, status badges)
  - Loading and empty states (including `.loading-spinner`)
  - Responsive breakpoints (640px, 768px, 1024px)
  - Chart height utilities (`.chart-h-250`, `.chart-h-300`, `.chart-h-400`)

**JavaScript Layer (`v2/js/`):**

- `affiliate-utils.js` - Shared utility functions:

  - `escapeHtml()` - XSS prevention
  - `formatCurrency()` - EUR formatting (German locale)
  - `formatDate()` - Date formatting (German locale)
  - `handleAPIError()` - Error handling with UI feedback
  - `showLoadingState()` - Loading state management
  - `showEmptyState()` - Empty state display

- `affiliate-charts.js` - Chart factory functions:

  - `createMRRLineChart()` - MRR trend line chart
  - `createMRRDonutChart()` - MRR status breakdown donut chart
  - `createBarChart()` - Generic bar chart
  - `createHorizontalBarChart()` - Horizontal bar chart

- `affiliate-dialogs.js` - In-platform dialogs (confirm and alert):
  - `showConfirm(options)` - Promise-based confirmation (title, message, confirmText, cancelText, type: warning/danger). Esc and backdrop close.
  - `showAlert(options)` - Single OK alert (title, message, type: error/info/success). Used by partner-admin, referral-urls, and partner-settings instead of native `alert()`/`confirm()`.

- `affiliate-program-analytics.js` - Admin program metrics page: loads `affiliate-admin-program-metrics.php`, presets/custom date range, URL query sync, Chart.js via `AffiliateCharts` factories.

**Page Templates (`v2/includes/`):**

- `affiliate-page-template.php` - Page wrapper function:
  - `renderAffiliatePage()` - Handles authentication, head section, sidebar, page structure
  - Reduces boilerplate code in partner pages
  - Supports custom head content and scripts

### Benefits

- **Code Reduction:** ~1,500+ lines of duplicated code eliminated
- **Single Source of Truth:** Common patterns centralized for easier maintenance
- **Consistency:** Uniform error handling, formatting, and styling across all pages
- **Performance:** Reduced HTML size, better browser caching of shared assets
- **Developer Experience:** Easier to add new pages/features using shared components

### Usage Pattern

**API Endpoints:**

```php
require_once __DIR__ . '/../helpers/affiliate-api-base.php';
require_once __DIR__ . '/../helpers/affiliate-data-formatters.php';

$partnerId = requireAffiliateAuthAPI();
$cacheData = loadCachedHubSpotData();
$partner = getAuthenticatedPartnerValidated();

// Use formatters
$leads = $cacheData['leads'][$partnerId] ?? [];
$formattedLeads = array_map('formatLeadForAPI', $leads);
$leadCounts = calculateLeadCounts($leads);

sendJSONResponse(true, ['leads' => $formattedLeads, 'counts' => $leadCounts]);
```

**JavaScript:**

```javascript
const Utils = window.AffiliateUtils || {};
const Charts = window.AffiliateCharts || {};

// Use utilities
const formatted = Utils.formatCurrency(1234.56); // "1.234,56"
const date = Utils.formatDate('2026-01-30'); // "30.01.2026"
const safe = Utils.escapeHtml('<script>alert("xss")</script>');

// Use chart factories
const chart = Charts.createMRRLineChart('myCanvas', { labels: [...], values: [...] });
```

**Page Template:**

```php
require_once __DIR__ . '/../includes/affiliate-page-template.php';

renderAffiliatePage(
    'Page Title',
    'active-nav-item',
    $pageContent,
    [
        'chartJs' => 'true',
        'metaDescription' => 'Page description',
        'customHead' => '<style>...</style>',
        'customScripts' => '<script>...</script>'
    ]
);
```

Complete technical architecture documentation for the Ordio Loop affiliate partner system.

## Overview

The Ordio Loop affiliate partner system is a comprehensive platform for managing affiliate partnerships, tracking referrals, calculating earnings, and providing partner dashboards. The system integrates with HubSpot CRM for lead and deal tracking.

**Important:** The affiliate partner system uses a **standalone architecture** that is completely separated from the main marketing website. This ensures no conflicts, better maintainability, and improved performance. See [Standalone Architecture](./STANDALONE_ARCHITECTURE.md) for complete details.

## System Architecture

### High-Level Architecture

```
┌─────────────────┐
│   Partner Web   │
│    Interface    │
└────────┬────────┘
         │
         ▼
┌─────────────────┐      ┌──────────────────┐
│   PHP Backend   │◄────┤  HubSpot CRM API │
│   (v2/pages/    │      │   (Hourly Sync)   │
│    v2/api/)     │      └──────────────────┘
└────────┬────────┘
         │
         ▼
┌─────────────────┐
│  JSON Storage   │
│  (Local Cache)  │
└─────────────────┘
```

### Cache-First Architecture

**Display APIs never call HubSpot.** All partner dashboards (Dashboard, Leads, Earnings, Levels, Leaderboard, Admin) read exclusively from `affiliate_hubspot_cache.json` via `loadCachedHubSpotData()`. HubSpot is called only during sync (cron or manual trigger).

| Display API | Data Source |
|-------------|-------------|
| `affiliate-dashboard-data.php` | `loadCachedHubSpotData()` → leads, deals |
| `affiliate-leads.php` | `loadCachedHubSpotData()` → leads |
| `affiliate-earnings.php` | `loadCachedHubSpotData()` → deals |
| `affiliate-levels.php` | `loadCachedHubSpotData()` → leads, deals, mrr_summary |
| `affiliate-leaderboard.php` | `loadCachedHubSpotData()` → mrr_summary |
| `affiliate-admin-list.php` | `loadCachedHubSpotData()` → leads, deals, mrr_summary |

**Verification:** Run `php v2/scripts/affiliate/audit-no-hubspot-in-display.php` to confirm no forbidden HubSpot calls in display APIs.

### API Response Caching

Display APIs set HTTP cache headers to reduce server load and improve repeat-visit performance. All use `private` (browser-only, no CDN) because responses are partner-specific or auth-required.

| API                         | Cache TTL | Rationale                                   |
|-----------------------------|-----------|---------------------------------------------|
| affiliate-dashboard-data    | 60 s      | HubSpot sync runs hourly; 1 min stale OK    |
| affiliate-leads             | 60 s      | Same as above                               |
| affiliate-earnings          | 60 s      | Same as above                               |
| affiliate-levels            | 60 s      | Same as above                               |
| affiliate-leaderboard       | 60 s      | Same as above                               |
| affiliate-sitemap-pages     | 300 s     | Sitemap changes rarely                       |

Helper: `setAffiliateAPICacheHeaders($maxAge)` in `affiliate-api-base.php`. Sitemap API sets header directly (does not use affiliate-api-base).

## Component Overview

### Frontend Components

**Location:** `v2/pages/`, `v2/js/`, `v2/css/`, `v2/base/`

- **Dashboard Pages (Standalone):**

  - `partner-dashboard.php` - Main dashboard
  - `partner-leads.php` - Leads management
  - `partner-earnings.php` - Earnings overview
  - `partner-referral-urls.php` - URL generation
  - `partner-leaderboard.php` - Leaderboard
  - `partner-settings.php` - Settings

- **Public Pages:**

  - `partner-program.php` - Marketing landing page (uses marketing site integration)
  - `partner-register.php` - Registration form (standalone)
  - `partner-login.php` - Login page (standalone)
  - `partner-reset-password.php` - Password reset (standalone)

- **Head Section:**

  - `affiliate-head.php` - Standalone head section for partner pages (no marketing dependencies)

- **JavaScript:**

  - `affiliate-dashboard.js` - Dashboard functionality (vanilla JS, no dependencies)
  - `affiliate-utils.js` - Shared utility functions (escapeHtml, formatCurrency, formatDate, error handling)
  - `affiliate-charts.js` - Chart factory functions for Chart.js (createMRRLineChart, createMRRDonutChart, createBarChart)
  - `utm-tracking.js` - UTM parameter tracking (extended, used on main site)

- **CSS:**

  - `affiliate-shared.css` - Shared CSS variables and common component styles (design tokens, sidebar, cards, tables, buttons)
  - `affiliate-dashboard.css` - Page-specific dashboard styles (standalone, no Tailwind dependencies)
  - `affiliate-admin.css` - Admin (Verwaltung) page: flat cards (no shadow/hover), partner table with column min-widths and horizontal scroll
  - `affiliate-dialogs.css` - In-platform confirm and alert dialogs (overlay, card, buttons); used by admin, referral-urls, settings

- **Dialogs (JS):**
  - `affiliate-dialogs.js` - `showConfirm(options)` and `showAlert(options)`; replaces native alert/confirm on partner-admin, referral-urls, partner-settings

### Backend Components

**Location:** `v2/api/`, `v2/helpers/`, `v2/includes/`, `v2/config/`

- **API Endpoints:**

  - `partner-register.php` - Partner registration
  - `partner-login.php` - Authentication
  - `partner-logout.php` - Session termination
  - `partner-reset-password.php` - Password reset
  - `partner-update-profile.php` - Profile updates
  - `partner-change-password.php` - Password changes
  - `partner-verify-email.php` - Email verification
  - `affiliate-dashboard-data.php` - Dashboard data
  - `affiliate-leads.php` - Leads data
  - `affiliate-earnings.php` - Earnings data
  - `affiliate-leaderboard.php` - Leaderboard data
  - `affiliate-generate-url.php` - URL generation
  - `affiliate-admin-list.php` - Admin: list partners with stats (auth + admin only)
  - `affiliate-admin-update-partner.php` - Admin: PATCH partner status / is_admin (auth + admin only)
  - `affiliate-admin-audit.php` - Admin: GET audit log entries (auth + admin only)

- **Helper Functions:**

  - `affiliate-auth.php` - Authentication logic
  - `affiliate-paths.php` - File path resolution
  - `affiliate-api-base.php` - Shared API functions (loadCachedHubSpotData, requireAffiliateAuthAPI, sendJSONResponse, handleAPIError)
  - `affiliate-data-formatters.php` - Data formatting functions (formatLeadForAPI, formatDealForAPI, calculateLeadCounts, formatMRRStatusBreakdown)
  - `hubspot-affiliate-api.php` - HubSpot API wrappers
  - `affiliate-mrr-calculator.php` - MRR calculations
  - `affiliate-level-calculator.php` - Level calculations
  - `affiliate-badges.php` - Badge system
  - `affiliate-last-active.php` - Persist last_login_at / last_active_at (throttled)
  - `affiliate-admin-audit.php` - Log admin actions to affiliate_admin_audit.json

- **Page Templates:**

  - `affiliate-page-template.php` - Page wrapper function (renderAffiliatePage) for consistent page structure
  - `affiliate-sidebar.php` - Sidebar navigation component

- **Configuration:**
  - `affiliate-config.php` - System configuration
  - `affiliate-badges.php` - Badge definitions

### Data Storage

**Location:** `v2/data/`, `writable/`

- **JSON Files:**

  - `affiliate_partners.json` - Partner registry (see Partner JSON optional fields below)
  - `affiliate_hubspot_cache.json` - HubSpot data cache
  - `affiliate_admin_audit.json` - Admin action audit log (same directory as partner data)

- **Storage Strategy:**
  - Multi-tier fallback system
  - Writable location priority
  - Readable location fallback
  - Atomic file writes

**Partner JSON optional fields:** All new partner fields are optional so existing records keep working. Optional fields include: `is_admin`, `terms_accepted_at`, `terms_version`, `last_login_at`, `last_active_at`. `last_login_at` is set on successful login; `last_active_at` is updated on authenticated requests but throttled (e.g. every 15 minutes) via `AFFILIATE_LAST_ACTIVE_UPDATE_INTERVAL` in config.

### Admin panel (Verwaltung)

Admins (config `AFFILIATE_ADMIN_EMAILS` or partner `is_admin: true`) see an **Admin** tab in the partner dashboard. The admin page (`partner-admin.php`) lists all partners with status, level, registration, **last activity** (last_active_at / last_login_at), leads, deals, MRR; admins can deactivate/activate partners and assign/revoke admin. **Program analytics:** `partner-program-analytics.php` (`/partner/program-analytics`) shows program-wide aggregates from the HubSpot cache via GET `affiliate-admin-program-metrics.php` (see [PROGRAM_ANALYTICS_GUIDE.md](./PROGRAM_ANALYTICS_GUIDE.md)). **List API:** GET `affiliate-admin-list.php` returns partners including `last_login_at`, `last_active_at`. **Update API:** PATCH `affiliate-admin-update-partner.php` for status and is_admin. **Audit:** Admin actions are logged to `affiliate_admin_audit.json`; GET `affiliate-admin-audit.php` returns recent entries. **CSV export:** Client-side export of the partner list (including Letzte Aktivität) from the list API data.

### Cron Jobs

**Location:** `v2/cron/`

- **Sync Script:**
  - `sync-affiliate-hubspot.php` - HubSpot affiliate sync (intended **hourly** on the app server at minute 0)
  - Fetches partners, leads, deals
  - Calculates MRR and levels
  - Updates cache

**How sync is triggered:** Production should use **server cron** (CLI) as the primary schedule. A **GitHub Actions** workflow (`.github/workflows/hubspot-sync-cron.yml`) may call `v2/api/cron-webhook-sync.php` **twice daily** as a backup; that webhook enforces a **5-minute** minimum between successful calls and is not meant for hourly HTTP polling. Admins can run **Sync mit HubSpot** from Verwaltung (same lock + runner). See [CRON_SYNC_RUNBOOK.md](CRON_SYNC_RUNBOOK.md) for SSOT.

## Data Flow

### Registration Flow

```
1. Partner submits registration form
   ↓
2. API validates input (name, email, password)
   ↓
3. Generate unique Partner ID (AP-YYYYMMDD-XXXXXX)
   ↓
4. Hash password with password_hash()
   ↓
5. Save to affiliate_partners.json
   ↓
6. Create HubSpot custom object
   ↓
7. Send verification email
   ↓
8. Return success response
```

### Lead Tracking Flow

**Important:** Partners refer Ordio users (leads/customers), not partner dashboard users. The system tracks these leads automatically.

```
1. Partner shares referral link (?affiliate=PARTNER_ID)
   ↓
2. User clicks link and browses Ordio website
   ↓
3. UTM tracker captures affiliate parameter
   ↓
4. Store affiliate ID in cookies/localStorage
   ↓
5. User submits lead form (demo booking, lead capture, etc.)
   ↓
6. Lead capture API reads affiliate from cookies
   ↓
7. Create HubSpot contact with affiliate_partner_id
   ↓
8. Set affiliate_referral_date property
   ↓
9. Lead appears in partner dashboard (after sync)
```

**Note:** We don't create deals automatically. Sales team creates deals when leads convert to customers, and manually sets affiliate properties on deals for MRR tracking.

### Sync Flow

**Important:** Sync reads data from HubSpot but doesn't create deals. Deals are created by sales team.

```
1. Scheduled sync runs (primary: hourly server cron at minute 0; backup: optional GitHub webhook 2×/day)
   ↓
2. Load partner registry from JSON
   ↓
3. For each partner:
   a. Fetch partner data from HubSpot custom object
   b. Fetch leads (contacts with affiliate_partner_id) - AUTOMATIC
   c. Fetch deals (deals with affiliate_partner_id) - SET BY SALES
   ↓
4. Calculate MRR for each partner (based on deals with affiliate properties)
   ↓
5. Calculate partner levels (rolling 90 days, based on deal count): **Beginner** (0 deals), **Starter** (1–5), **Partner** (6–10), **Pro** (11+). New registrations and unactivated partners are Beginner until they have at least one deal in the last 90 days. **Manual override:** If partner has `level_override` true and `level_override_until` not past, use stored level; sync does not overwrite. If `level_override_until` is in the past, sync clears override and reverts to calculated.
   ↓
6. Update cache file (affiliate_hubspot_cache.json)
   ↓
7. Log sync results
```

**Note:**

- Leads (contacts) are tracked automatically when users submit forms
- Deals must be created by sales team and manually attributed to partners
- See [HUBSPOT_SALES_WORKFLOW.md](HUBSPOT_SALES_WORKFLOW.md) for sales team guide

### Dashboard Data Flow

```
1. Partner logs in and accesses dashboard
   ↓
2. Frontend calls affiliate-dashboard-data.php
   ↓
3. API loads cached HubSpot data
   ↓
4. Filter data for authenticated partner
   ↓
5. Calculate KPIs (leads, deals, MRR)
   ↓
6. Format data for display
   ↓
7. Return JSON response
   ↓
8. Frontend renders dashboard with Chart.js
```

## HubSpot Integration

### Custom Objects

**Object Type:** `affiliate_partner`

**Properties:**

- `partner_id` (text) - Partner identifier
- `name` (text) - Partner name
- `email` (text) - Partner email
- `level` (enum) - Partner level (beginner/starter/partner/pro). New registrations and 0-deal partners use **Beginner**; HubSpot Level property must include "Beginner" (run `v2/scripts/hubspot/update-affiliate-level-property.php` once if needed).
- `mrr_share_percent` (number) - MRR share percentage
- `status` (enum) - Partner status
- `registration_date` (date) - Registration timestamp

### Contact Properties

**Custom Properties:**

- `affiliate_partner_id` (text) - Referral partner ID
- `affiliate_referral_date` (date) - Referral timestamp

### Deal Properties

**Custom Properties:**

- `affiliate_partner_id` (text) - Deal attribution partner
- `affiliate_mrr` (number) - Deal MRR amount
- `affiliate_subscription_status` (enum) - Subscription status

### API Endpoints Used

- **CRM API v3:**

  - `GET /crm/v3/objects/{objectType}` - Fetch objects
  - `POST /crm/v3/objects/{objectType}` - Create objects
  - `PATCH /crm/v3/objects/{objectType}/{objectId}` - Update objects

- **Forms API v3:**
  - `POST /submissions/v3/integration/secure/submit/{portalId}/{formGuid}` - Submit forms

### HubSpot Setup Requirements

**Required API Scopes:**

The HubSpot private app token must have the following scopes:

- `crm.objects.custom.read` - Read custom objects
- `crm.objects.custom.write` - Write custom objects
- `crm.schemas.custom.read` - Read custom object schemas
- `crm.schemas.custom.write` - Write custom object schemas
- `crm.objects.contacts.read` - Read contacts
- `crm.objects.contacts.write` - Write contacts
- `crm.objects.deals.read` - Read deals
- `crm.objects.deals.write` - Write deals

**Setup Process:**

1. **Verify API Token:**

   ```bash
   php v2/scripts/hubspot/test-api-token-scopes.php
   ```

2. **Create Property Group (Manual):**

   - Go to HubSpot Settings → Properties
   - Create property group `affiliate_info` for Contacts
   - Create property group `affiliate_info` for Deals
   - Or run: `php v2/scripts/hubspot/create-affiliate-property-group.php`

3. **Create Custom Object:**

   ```bash
   php v2/scripts/hubspot/create-affiliate-custom-object.php
   ```

4. **Create Properties:**

   ```bash
   php v2/scripts/hubspot/setup-affiliate-properties.php
   ```

5. **Verify Setup:**
   ```bash
   php v2/scripts/hubspot/verify-affiliate-setup.php
   ```

**Automated Setup:**

Run the complete setup automation script:

```bash
php v2/scripts/hubspot/setup-affiliate-hubspot-complete.php
```

This script runs all setup steps in order with verification.

**Property Group:**

- **Name:** `affiliate_info`
- **Applies to:** Contacts and Deals
- **Note:** Property group creation requires manual setup in HubSpot UI (API does not support it)

**Custom Object Type ID:**

After creating the custom object, the object type ID will be `2-affiliate_partner` (or similar). This is stored in `HUBSPOT_AFFILIATE_OBJECT_TYPE` constant in `v2/config/affiliate-config.php`.

**Verification:**

Use the verification script to check setup status:

```bash
php v2/scripts/hubspot/verify-affiliate-setup.php
```

For detailed setup instructions, see [HUBSPOT_SETUP.md](HUBSPOT_SETUP.md).

## Authentication System

### Session Management

- **Session Storage:** PHP `$_SESSION`
- **Session Timeout:** 30 minutes inactivity
- **Session Regeneration:** On login for security
- **Remember Me:** Cookie-based (30 days)

### Password Security

- **Hashing:** `password_hash()` with `PASSWORD_DEFAULT`
- **Verification:** `password_verify()`
- **Requirements:** Minimum 8 characters
- **Reset:** Token-based (1 hour expiry)

### Authentication Flow

```
1. Partner submits login form
   ↓
2. API validates credentials
   ↓
3. Verify password with password_verify()
   ↓
4. Check login attempts (max 5)
   ↓
5. Set session variables:
   - affiliate_authenticated = true
   - affiliate_partner_id = partner_id
   - affiliate_last_activity = timestamp
   ↓
6. Regenerate session ID
   ↓
7. Set remember me cookie (if requested)
   ↓
8. Redirect to dashboard
```

## MRR Calculation

### Formula

```
Partner MRR = Σ(Deal MRR × Partner Level MRR Share %) for all active deals
```

### Level-Based Shares

- **Starter (1–5 deals in 90 days):** 20% MRR share
- **Partner (6–10 deals in 90 days):** 25% MRR share
- **Pro (11+ deals in 90 days):** 30% MRR share

### Calculation Logic

1. Filter deals by partner ID
2. Filter deals by status (won deals only)
3. Get deal MRR from `affiliate_mrr` property
4. Apply partner level MRR share percentage
5. Sum all partner MRR contributions
6. Categorize by subscription status (active/paused/cancelled)

## Level Calculation

### Rolling 90-Day Calculation

- Levels calculated based on **won deals in the last 90 days**
- Calculation period: Rolling 90-day window (today minus 90 days to today)
- Updates automatically after each sync (no fixed quarter reset)

### Level Thresholds

- **Starter:** 1–5 deals in 90 days
- **Partner:** 6–10 deals in 90 days
- **Pro:** 11+ deals in 90 days

### Calculation Flow

```
1. Get all won deals closed in the last 90 days
   ↓
2. Count deals attributed to partner
   ↓
3. Determine level based on deal count:
   - 1–5 deals → Starter
   - 6–10 deals → Partner
   - 11+ deals → Pro
   ↓
4. Update partner level in registry (unless manual override active)
   ↓
5. Update HubSpot custom object with effective level (manual or calculated)
```

### Manual Override

- Admins can set `level_override` and optional `level_override_until` on a partner via the Admin UI or API
- **Admin set/revert:** Platform JSON and HubSpot are updated immediately (PATCH). Manual level overrides HubSpot automated syncs.
- When override is active, sync uses stored `level` and pushes it to HubSpot; it does not overwrite with calculated values
- When `level_override_until` is in the past, sync auto-clears override and reverts to calculated
- Revert: Admin sets `level_override: false`; HubSpot PATCHed immediately with calculated level
- See [LEVEL_CALCULATION.md](LEVEL_CALCULATION.md#manual-override) for details

## File Storage System

### Multi-Tier Fallback

The system uses a multi-tier fallback for file storage:

1. **Primary:** `writable/data/` (if writable)
2. **Secondary:** `v2/data/` (if writable)
3. **Fallback:** Read-only from any available location

### File Operations

- **Read:** Check all locations, use first available
- **Write:** Use writable location, create if needed
- **Atomic Writes:** Write to `.tmp` file, then rename

### File Structure

```
affiliate_partners.json:
{
  "version": "1.0",
  "last_updated": "2026-01-29T10:00:00Z",
  "partners": {
    "AP-20260129-ABC123": {
      "partner_id": "AP-20260129-ABC123",
      "name": "Partner Name",
      "email": "partner@example.com",
      "password_hash": "$2y$10$...",
      "status": "active",
      "level": "starter",
      "registration_date": "2026-01-29T10:00:00Z",
      "is_admin": false,
      "terms_accepted_at": "2026-01-29T10:00:00Z",
      "last_login_at": "2026-02-01T14:00:00+00:00",
      "last_active_at": "2026-02-01T14:05:00+00:00",
      ...
    }
  }
}

affiliate_hubspot_cache.json:
{
  "version": "1.0",
  "last_sync": "2026-01-29T02:00:00Z",
  "partners": {...},
  "leads": {...},
  "deals": {...},
  "mrr_summary": {...}
}
```

## Security Considerations

### Input Validation

- All user inputs sanitized
- Email validation with `filter_var()`
- Password strength requirements
- SQL injection prevention (no SQL used)
- XSS prevention with `htmlspecialchars()`

### Session Security

- Session regeneration on login
- Session timeout enforcement
- Secure cookie flags (when implemented)
- CSRF protection (when implemented)

### API Security

- Rate limiting (login attempts)
- Lockout after failed attempts
- Token-based password reset
- Secure password hashing

## Performance Optimizations

### Caching Strategy

- **HubSpot Data:** Cached locally in JSON
- **Hourly Sync:** Reduces API calls
- **Dashboard Data:** Loaded from cache
- **Reduced Latency:** Faster dashboard loads

### API Optimization

- **Batch Requests:** Fetch multiple partners at once
- **Selective Properties:** Only fetch needed properties
- **Pagination:** Handle large datasets
- **Error Handling:** Retry logic for failed requests

## Testing

### Test Scripts

**Location:** `tests/affiliate/`

- `partner-registration-test.php` - Registration tests
- `hubspot-sync-test.php` - Sync functionality tests
- `mrr-calculation-test.php` - MRR calculation tests

### Test Coverage

- Partner ID generation
- Email validation
- Password hashing
- MRR calculation logic
- Level calculation logic
- File path resolution
- Authentication flow

## Deployment

### Requirements

- PHP 7.4+
- HubSpot API access
- Writable data directory
- Cron job capability

### Setup Steps

1. **Configure HubSpot:**

   - Create custom object schema
   - Create custom properties
   - Set up API access

2. **Configure System:**

   - Update `affiliate-config.php`
   - Set HubSpot API token
   - Configure file paths

3. **Set Up Cron:**

   - Schedule hourly sync job
   - Test sync manually first

4. **Test Registration:**
   - Create test partner
   - Verify HubSpot integration
   - Test dashboard access

## Related Documentation

- **[Partner Guide](PARTNER_GUIDE.md)** - User guide for partners
- **[Dashboard Guide](DASHBOARD_GUIDE.md)** - Dashboard usage guide
- **[API Reference](API_REFERENCE.md)** - API endpoint documentation
