# Deleting or Deactivating Partners (Admin)

**Last Updated:** 2026-03-06

The partner platform uses **two separate data stores**. Deleting a partner in one does not automatically affect the other.

## Admin tab (partner dashboard)

Admins have a **Verwaltung** (Admin) tab in the partner dashboard (next to Einstellungen). Only users with admin rights can see and open it.

- **Bootstrap admin:** `hady@ordio.com` is admin by default via config (`AFFILIATE_ADMIN_EMAILS` in `v2/config/affiliate-config.php`). No JSON edit is required for the first admin.
- **Assigning admins:** On the Admin page, admins can assign the admin role to any partner (dropdown “Admin zuweisen”). Assigned admins are stored in the partner record as `is_admin: true`.
- **Removing admin:** Admins can revoke the admin role (“Admin entziehen”) for partners who were assigned via the UI. Config bootstrap admins (e.g. hady@ordio.com) cannot be unassigned; at least one admin must always remain.
- **Deactivate / reactivate:** On the Admin page, admins can set a partner’s status to **Deaktiviert** or **Aktiv**. Deactivated partners cannot log in. When you **reactivate** (Aktivieren): partners who **have verified their email** (`email_verified_at` set) become **Aktiv** and can log in; partners who **never verified** are restored to **Ausstehend** (pending_verification) so they must still verify before they can log in (email verification is not bypassed). See [TROUBLESHOOTING.md](TROUBLESHOOTING.md#partner-shows-pending-after-i-reactivated) if a partner shows Pending after reactivate.
- **Real-time effect:** Auth re-checks partner status on every request. As soon as an admin deactivates an account, that user loses access on their next request (session is cleared when `status !== 'active'`). No need to wait for session timeout.
- **Letzte Aktionen (audit log):** The Admin page shows a “Letzte Aktionen” section with the last 50 admin actions (who deactivated/activated whom, who assigned/revoked admin). Log is stored in `affiliate_admin_audit.json` (same directory as the partner data file); see `v2/helpers/affiliate-admin-audit.php` and GET `/v2/api/affiliate-admin-audit.php`.
- **CSV export:** “Partner als CSV exportieren” downloads a CSV of the filtered partner list (current search and status; same columns as the table: Name, E-Mail, Partner-ID, Status, Level, Registrierung, Letzte Aktivität, Leads, Deals, MRR). Export is generated client-side from the list API data.
- **Last active / last login:** Partner records can include optional `last_login_at` and `last_active_at` (ISO 8601). `last_login_at` is set on successful login; `last_active_at` is updated on authenticated requests but **throttled** (e.g. at most every 15 minutes per partner) via `AFFILIATE_LAST_ACTIVE_UPDATE_INTERVAL` in `v2/config/affiliate-config.php` to limit JSON write load. The Admin table and CSV show a "Letzte Aktivität" column (uses `last_active_at`, fallback to `last_login_at`, then "–"). Optional backfill for existing partners: `php v2/scripts/affiliate/backfill-last-active.php [--dry-run]` sets missing fields to `registration_date` or null.
- **Admin page styling:** Flat cards (no shadow/hover), table with horizontal scroll and column min-widths so columns are not squished. Styles: `v2/css/affiliate-admin.css` (minified: `affiliate-admin.min.css`). Run `npm run minify` after editing.
- **Table actions:** The Aktionen column uses icon-only buttons (Level setzen, Deaktivieren, Aktivieren, Admin entziehen, Admin zuweisen, Löschen) with `title` and `aria-label` tooltips on hover/focus for compact layout and accessibility.
- **Delete partner:** Admins can permanently delete partners via the **Löschen** button. This removes the partner from JSON, cleans up cache and tokens, and logs the action. Bootstrap admins cannot be deleted. Self-deletion is prevented. At least one admin must remain.
- **Confirmations and errors:** Deactivate, revoke admin, and all error/info messages use **in-platform dialogs** (confirm and alert modals) instead of browser `alert()`/`confirm()`. Styled with `v2/css/affiliate-dialogs.css` (minified: `affiliate-dialogs.min.css`).
- **Sync mit HubSpot:** The "Sync mit HubSpot" button in the Partner section toolbar runs the same HubSpot sync as the hourly cron (partner, lead, deal data and level/MRR push). It does not change the cron schedule; the same lock prevents manual and cron from running at once. Rate limit: one manual sync per 5 minutes.
- **Search, filter, pagination:** Partner table supports search (name, E-Mail, Partner-ID), Status filter (Alle, Aktiv, Deaktiviert, Ausstehend), Level filter (Beginner, Starter, Partner, Pro), and pagination (Pro Seite 10/20/50/100; Vorherige/Nächste, page numbers, jump). All client-side. CSV export uses the **filtered** list (current search + status). Pending and 0-deal partners show as **Beginner**.
- **Troubleshooting 403:** If you see "Keine Admin-Rechte" (403) when assigning an admin, see [TROUBLESHOOTING.md](TROUBLESHOOTING.md#troubleshooting-403-when-assigning-admin).

## HubSpot vs platform

| Store                                          | Purpose                | Delete here?                                                                                               |
| ---------------------------------------------- | ---------------------- | ---------------------------------------------------------------------------------------------------------- |
| **HubSpot** (Affiliate Partners custom object) | CRM, reporting, sync   | Deleting in HubSpot only removes the HubSpot record. The partner can still log in to the partner platform. |
| **Platform** (JSON file)                       | Login, dashboard, auth | This is the source of truth for who can log in. You must change or edit this to remove access.             |

**Conclusion:** To remove a partner from the platform (so they can’t log in), you must change or remove their record in the **platform JSON**, not only in HubSpot.

## Options

### 1. Deactivate (recommended – soft delete)

Partner can no longer log in; record stays for audit.

- **By script (recommended):**
  ```bash
  php v2/scripts/affiliate/deactivate-partner.php --email=partner@example.com
  # or
  php v2/scripts/affiliate/deactivate-partner.php --partner-id=AP-20260129-AD2F73
  ```
- **By hand:** Open the partner data file (see [Data file location](#data-file-location)), find the partner by `partner_id` or `email`, and set `"status"` to `"deactivated"` (or any value other than `"active"`). Save the file.

After that, login will fail with “Account is not active” because `affiliate-auth.php` only allows `status === 'active'`.

### 2. Remove (hard delete)

Partner is removed from the JSON; they can’t log in and no longer appear in the platform data.

**Hard delete removes:**
- Partner record from `affiliate_partners.json`
- Partner data from cache (`affiliate_hubspot_cache.json` - partners, leads, deals, mrr_summary)
- Remember-me tokens (`affiliate_remember_tokens.json`)
- HubSpot custom object record (automatically deleted)
- Audit log entry is created

- **By Admin UI (recommended):**
  1. Log in as admin
  2. Go to **Verwaltung** → **Partner** section
  3. Find the partner in the table
  4. Click **Löschen** button in the **Aktionen** column
  5. Confirm deletion in the dialog
  6. Partner is immediately removed (including HubSpot object)

- **By script:**
  ```bash
  php v2/scripts/affiliate/deactivate-partner.php --email=partner@example.com --remove
  # or
  php v2/scripts/affiliate/deactivate-partner.php --partner-id=AP-20260129-AD2F73 --remove
  ```
  **Note:** CLI script does not delete HubSpot objects - use Admin UI or API endpoint for complete deletion.

- **By hand:** Open the partner data file, delete the whole partner object under `partners[<partner_id>]`, save the file. **Note:** Manual deletion does not clean up cache, tokens, or HubSpot objects automatically.

### 3. HubSpot only (optional)

If you only want to remove the partner from HubSpot (e.g. for CRM hygiene) but **keep** platform access:

- In HubSpot: **Contacts → Affiliate Partners** (or your custom object list), open the record, use the record’s **Delete** (often under **More** or in the record menu).

This does **not** stop them from logging in; use option 1 or 2 for that.

## Data file location

The platform reads/writes partner data from a JSON file. Resolved in this order:

1. `v2/data/affiliate_partners.json`
2. `writable/affiliate_partners.json`
3. Other fallbacks (see `v2/includes/affiliate-paths.php`)

Use the same file for manual edits that the script would use; the script uses `findWritableAffiliateDataFile()` / `findReadableAffiliateDataFile()`.

## Updating partner records in production (admin, etc.)

Pre-existing partners may not have `is_admin` or other optional fields set. You can update them in three ways:

1. **Admin UI (preferred when you can log in as admin)**  
   Log in as a config admin (e.g. hady@ordio.com, see `AFFILIATE_ADMIN_EMAILS` in config). Open **Verwaltung** → use **Neuen Admin zuweisen** and **Admin zuweisen** to assign admin to a partner. The API writes `is_admin: true` to the partner record. Ensure the admin page and API are on the same origin (see [LOCAL_DEV.md](LOCAL_DEV.md)); if you see 403, see [TROUBLESHOOTING.md](TROUBLESHOOTING.md).

2. **CLI script (production or when UI is not available)**  
   On the server (e.g. via SSH), run:

   ```bash
   php v2/scripts/affiliate/set-partner-admin.php --email=partner@example.com --is-admin=1
   # or
   php v2/scripts/affiliate/set-partner-admin.php --partner-id=AP-20260129-AD2F73 --is-admin=1
   ```

   Use `--is-admin=0` to revoke admin. Use `--dry-run` to preview without writing. The script uses the same data file as the app (`findWritableAffiliateDataFile()`).

3. **Manual JSON edit**  
   Open the partner data file (see [Data file location](#data-file-location)), find the partner by `partner_id` or `email`, and set `"is_admin": true` (or `false`). Save the file. Use the same file the app uses so changes are picked up immediately.

## API Endpoint: DELETE Partner

**Endpoint:** `/v2/api/affiliate-admin-update-partner.php`  
**Method:** `DELETE`  
**Authentication:** Required (admin only)

### Request

```json
{
  "partner_id": "AP-20260129-AD2F73"
}
```

### Response

**Success (200):**
```json
{
  "success": true,
  "message": "Partner wurde erfolgreich gelöscht."
}
```

**Error (400):**
```json
{
  "success": false,
  "error": "Das Haupt-Admin-Konto kann nicht gelöscht werden."
}
```

**Error (404):**
```json
{
  "success": false,
  "error": "Partner not found"
}
```

### Protection Rules

The DELETE endpoint enforces the following protections:

1. **Bootstrap admin protection:** Cannot delete partners whose email is in `AFFILIATE_ADMIN_EMAILS` config
2. **Self-deletion protection:** Cannot delete your own account
3. **Last admin protection:** Cannot delete a partner if they are an admin and only one admin exists
4. **Admin-only:** Only authenticated admins can call this endpoint

### HubSpot Deletion

**HubSpot custom object is automatically deleted** when a partner is deleted via the admin UI or DELETE API endpoint. The deletion process:

1. Searches for the partner in HubSpot by `partner_id`
2. If found, deletes the HubSpot custom object record
3. If not found, logs a message (not an error - partner may not have been synced)
4. Deletion failures are logged but don't prevent partner deletion from completing

**Note:** HubSpot deletion is non-blocking - if HubSpot API is unavailable or returns an error, the partner is still deleted from the platform. Manual HubSpot cleanup may be needed in rare cases.

## After deactivating or removing

- **HubSpot:** When using **Admin UI delete**, HubSpot objects are automatically deleted. When using CLI script or manual JSON edit, HubSpot objects remain (use Admin UI or API endpoint for complete deletion). The next run of the affiliate–HubSpot sync will no longer include deleted partners in cache.
- **Sessions:** When using the **Admin tab** to deactivate a partner, access is revoked on the user’s next request (auth re-validates status each time). When using the CLI script or manual JSON edit, the partner may remain logged in until their next request or session timeout; the next time the server checks their status they will be logged out.
