# affiliate-hubspot Full Instructions

## HubSpot Custom Objects

### Affiliate Partner Object

**Object Type:** `affiliate_partner`  
**Type ID:** `2-affiliate_partner`

**Required Properties:**

- `partner_id` (text) - Partner identifier (AP-YYYYMMDD-XXXXXX)
- `name` (text) - Partner name
- `email` (text) - Partner email
- `level` (enum) - Partner level (starter/partner/pro)
- `mrr_share_percent` (number) - MRR share percentage
- `status` (enum) - Partner status (active/inactive)
- `registration_date` (date) - Registration timestamp

**Operations:**

- Create on partner registration
- Update on level changes
- Update on profile changes
- Fetch during sync

### Contact Properties

**Custom Properties:**

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

**Usage:**

- Set when lead is captured
- Used to attribute leads to partners
- Fetched during sync for lead tracking

### Deal Properties

**Custom Properties:**

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

**Usage:**

- Set when deal is won
- Used for MRR calculation
- Fetched during sync for earnings

## HubSpot API Integration

### API Helper Functions

**Location:** `v2/helpers/hubspot-affiliate-api.php`

**Functions:**

- `createAffiliatePartner()` - Create partner object
- `updateAffiliatePartner()` - Update partner object
- `getAffiliatePartner()` - Fetch partner object
- `getAffiliateLeads()` - Fetch leads for partner
- `getAffiliateDeals()` - Fetch deals for partner

**Pattern:**

```php
require_once __DIR__ . '/../config/hubspot-config.php';
require_once __DIR__ . '/../config/hubspot-api-helpers.php';

$result = makeHubSpotAPICall(
    $url,
    'POST',
    $headers,
    json_encode($payload),
    [
        'maxRetries' => 3,
        'timeout' => 30,
        'logPrefix' => 'affiliate-partner'
    ]
);
```

### API Calls

**Always Use:**

- `makeHubSpotAPICall()` helper function
- Retry logic (max 3 retries)
- Proper error handling
- Logging for debugging

**Never:**

- Direct cURL calls (use helper)
- Skip error handling
- Ignore rate limits
- Expose API tokens

## Sync Process

### Hourly Sync

**Location:** `v2/cron/sync-affiliate-hubspot.php`

**Schedule:** Hourly at minute 0

**Process:**

1. Load partner registry from JSON
2. For each partner:
   - Fetch partner data from HubSpot
   - Fetch leads (contacts with `affiliate_partner_id`)
   - Fetch deals (deals with `affiliate_partner_id`)
3. Calculate MRR for each partner
4. Calculate partner levels (quarterly)
5. Update cache file
6. Log sync results

**Error Handling:**

- Continue on individual partner errors
- Log all errors with context
- **Sync failure alerting:** On exit 1 (not `sync_already_running`), email sent to `hady@ordio.com` with error details. Uses `sendAffiliateEmail()`.
- Retry failed operations

**Production crontab:** Use `v2/cron/crontab-production.txt` (sync, health check, performance). Logs: `/var/log/affiliate-sync.log`, `/var/log/affiliate-sync-health.log`.

**Health check cron:** Runs daily at 9:00 UTC. Script: `v2/scripts/affiliate/monitor-sync-health.php`. Verify cron: `php v2/scripts/affiliate/verify-cron-installed.php`.

**When changing sync schedule or lock behavior:** Update the script docblock in `v2/cron/sync-affiliate-hubspot.php`, [DEPLOYMENT_CHECKLIST.md](docs/systems/affiliate/DEPLOYMENT_CHECKLIST.md), [HUBSPOT_SETUP_STATUS.md](docs/systems/affiliate/HUBSPOT_SETUP_STATUS.md), and [CRON_SYNC_RUNBOOK.md](docs/systems/affiliate/CRON_SYNC_RUNBOOK.md).

**Sync only processes partners in the registry:** Partners must exist in `affiliate_partners.json` with `status === 'active'` to be synced. When data is not updating for a specific partner, run `php v2/scripts/affiliate/diagnose-sync-frederik.php` to check registry state, HubSpot fetch result, cache state, and paths. See [CRON_SYNC_RUNBOOK.md](docs/systems/affiliate/CRON_SYNC_RUNBOOK.md) "No data for specific partner" and [HUBSPOT_INTEGRATION_TEST_PROCEDURE.md](docs/systems/affiliate/HUBSPOT_INTEGRATION_TEST_PROCEDURE.md) "Sync not updating data".

### Cache Management

**Cache File:** `affiliate_hubspot_cache.json`

**Structure:**

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

**Usage:**

- Dashboard loads from cache
- Reduces API calls
- Faster response times
- Hourly updates via sync

## MRR Calculation

### Calculation Logic

**Formula:**

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

**Steps:**

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

**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

### MRR Calculator

**Location:** `v2/helpers/affiliate-mrr-calculator.php`

**Function:** `calculatePartnerMRR($deals, $partnerId, $level)`

**Returns:**

```php
[
    'total_mrr' => 100.00,
    'active_mrr' => 75.00,
    'paused_mrr' => 15.00,
    'cancelled_mrr' => 10.00,
    'active_deals' => 3,
    'paused_deals' => 1,
    'cancelled_deals' => 1,
    'total_deals' => 5,
    'mrr_share_percent' => 25
]
```

**Edge Cases:**

- Handle missing MRR property (fallback to amount)
- Filter zero/negative MRR
- Only count won deals
- Handle missing subscription status

## Level Calculation

### Rolling 90-Day Calculation

**Location:** `v2/helpers/affiliate-level-calculator.php`

**Functions:** `countDealsInRolling90Days($deals, $partnerId)`, `calculateAffiliateLevel($dealCount)`

**Thresholds:**

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

**Process:**

1. Get all won deals closed in the last 90 days
2. Count deals attributed to partner (filter by closedate in rolling window)
3. Determine level based on deal count
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` via Admin UI or API
- When override active, sync uses stored level and does not overwrite
- Sync always pushes effective level (manual or calculated) to HubSpot
- See LEVEL_CALCULATION.md and ARCHITECTURE.md

**Timing:**

- Calculated on rolling 90-day window
- Updates after each sync
- No fixed quarter reset; levels can go up or down based on recent performance

## Property Management

### Setting Properties

**On Lead Capture:**

```php
$properties = [
    HUBSPOT_PROPERTY_AFFILIATE_PARTNER_ID => $partnerId,
    HUBSPOT_PROPERTY_AFFILIATE_REFERRAL_DATE => date('c')
];
```

**On Deal Creation:**

```php
$properties = [
    HUBSPOT_PROPERTY_AFFILIATE_PARTNER_ID => $partnerId,
    HUBSPOT_PROPERTY_AFFILIATE_MRR => $mrrAmount,
    HUBSPOT_PROPERTY_AFFILIATE_SUBSCRIPTION_STATUS => 'active'
];
```

### Property Names

**Use Constants:**

- `HUBSPOT_PROPERTY_AFFILIATE_PARTNER_ID`
- `HUBSPOT_PROPERTY_AFFILIATE_REFERRAL_DATE`
- `HUBSPOT_PROPERTY_AFFILIATE_MRR`
- `HUBSPOT_PROPERTY_AFFILIATE_SUBSCRIPTION_STATUS`

**Never Hardcode:**

- Property names may change
- Use constants from config
- Centralized management

## Best Practices

### API Calls

- **Use Helper Functions** - Always use `makeHubSpotAPICall()`
- **Retry Logic** - Implement retry for failed calls
- **Rate Limiting** - Respect HubSpot rate limits
- **Error Handling** - Handle all error cases
- **Logging** - Log all API interactions

### Data Consistency

- **Sync Regularly** - Hourly sync ensures fresh data
- **Cache Locally** - Reduce API calls
- **Validate Data** - Check data before processing
- **Handle Missing Data** - Graceful fallbacks

### Performance

- **Batch Requests** - Fetch multiple partners at once
- **Selective Properties** - Only fetch needed properties
- **Pagination** - Handle large datasets
- **Caching** - Cache frequently accessed data

### Error Handling

- **Continue on Errors** - Don't stop sync on single failure
- **Log Errors** - Detailed error logging
- **Alert on Critical** - Email alerts for failures
- **Retry Failed** - Retry logic for transient errors

## Common Patterns

### Fetching Partner Data

```php
$result = getAffiliatePartner($partnerId);
if ($result['success']) {
    $partnerData = $result['data'];
} else {
    // Handle error
}
```

### Calculating MRR

```php
$deals = getAffiliateDeals($partnerId);
$partner = getPartnerById($partnerId);
$level = $partner['level'] ?? 'starter';

$mrr = calculatePartnerMRR($deals, $partnerId, $level);
```

### Updating Level

```php
$dealCount = countWonDealsThisQuarter($partnerId);
$newLevel = calculateAffiliateLevel($dealCount);

if ($newLevel !== $currentLevel) {
    updatePartnerLevel($partnerId, $newLevel);
    updateHubSpotPartnerLevel($partnerId, $newLevel);
}
```

## Testing

**When testing:** Use `seed-affiliate-test-data.php` for frederik@ordio.com; run sync; verify dashboard; run `cleanup-affiliate-test-data.php`. See [HUBSPOT_INTEGRATION_TEST_PROCEDURE.md](../../docs/systems/affiliate/HUBSPOT_INTEGRATION_TEST_PROCEDURE.md).

## Related Documentation

- **[Architecture](../../docs/systems/affiliate/ARCHITECTURE.md)** - System architecture
- **[API Reference](../../docs/systems/affiliate/API_REFERENCE.md)** - API endpoints
- **[HubSpot Integration Test Procedure](../../docs/systems/affiliate/HUBSPOT_INTEGRATION_TEST_PROCEDURE.md)** - Test data seeding and verification
- **[Dashboard Rules](affiliate-dashboard.mdc)** - Dashboard patterns
