# CLAUDE.md

This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.

## Resources

- [WordPress Plugin Developer Handbook](https://developer.wordpress.org/) — reference for WordPress APIs (hooks, options, HTTP, settings, security, etc.)

## What this is

A WordPress plugin that integrates WordPress sites with a Post Affiliate Pro (PAP) account. It handles affiliate signup, click tracking, sale tracking, and integrations with a large number of e-commerce and membership plugins.

## No build system or test suite

This is a plain PHP WordPress plugin. There are no `composer.json`, `package.json`, build steps, or automated tests. Development requires a running WordPress installation with the plugin installed under `wp-content/plugins/postaffiliatepro/`.

The plugin requires `PapApi.class.php` to exist at the plugin root — this file is **not committed to the repo** and must be downloaded from the PAP merchant panel (Tools → Integration → API Integration → Download PAP API). Without it, the plugin silently disables itself (`apiFileExists()` check in the constructor).

## PAP API v1 (current — will be replaced by v3)

The plugin communicates with the PAP installation through `PapApi.class.php` (version constant `PAP_API_VERSION = '1.0.0.5'`). This file is **not in the repo** — it is downloaded from the PAP merchant panel. All API traffic goes to the PAP server's `scripts/server.php` endpoint using a JSON-RPC-style protocol.

**Session / auth**
- `Pap_Api_Session` (extends `Gpf_Api_Session`) — obtained once per request via `postaffiliatepro_Base::getApiSession()`. Login is done with merchant username + password. The plugin also supports login keys (`Pap_Auth_LoginKeyService`) for generating one-time affiliate panel URLs.

**RPC request types** — all extend `Gpf_Rpc_Request` and call `sendNow()`:
| Class | Used for |
|---|---|
| `Gpf_Rpc_FormRequest` | Single-object mutations (status change, refund, login key) |
| `Gpf_Rpc_GridRequest` | Paginated/filtered list queries (transactions, campaigns, affiliates) |
| `Gpf_Rpc_DataRequest` | Single key/value data fetch (e.g. hash script name) |
| `Gpf_Rpc_ActionRequest` | Fire-and-forget actions |

Params are passed with `$request->addParam($name, $value)`. List values use `Gpf_Rpc_Array`. Grid filters use `Gpf_Data_Filter` constants (`LIKE`, `EQUALS`, `DATERANGE_IS`, etc.).

**Domain objects**
- `Pap_Api_Affiliate` / `Pap_Api_AffiliateSignup` — load/save/add affiliates; `Pap_Api_AffiliatesGrid` for queries
- `Pap_Api_Transaction` — `approveByOrderId()` / `declineByOrderId()` (requires PAP ≥ 4.5.67.3)
- `Pap_Api_TransactionsGrid` — filter transactions by order ID or status; returns a `Gpf_Data_RecordSet`
- `Pap_Api_RecurringCommission` — `createCommissions()` to fire recurring commission for a subscription order ID
- `Pap_Api_ClickTracker` — server-side click tracking to resolve the referring affiliate from cookies

**Sale tracking (client-side)** is done by injecting JavaScript (`PostAffTracker` / `sale.php` calls) into the page, not through `PapApi.class.php`. Server-side sale tracking (used for offline/IPN flows) calls `sale.php` directly via `postaffiliatepro_Base::sendRequest()`.

**Planned migration:** the plugin will be migrated from this RPC-based API v1 to PAP API v3 (REST-based). When working on integration code, avoid deepening dependencies on `Gpf_Rpc_*` / `Pap_Api_*` classes beyond what already exists.

## Class architecture

All classes use the `postaffiliatepro_` prefix. The inheritance chain is:

```
postaffiliatepro_Base          ← Base.class.php
├── postaffiliatepro           ← postaffiliatepro.php (main plugin class)
├── postaffiliatepro_Form_Base ← Form/Base.class.php (abstract form renderer)
│   └── postaffiliatepro_Form_Settings_* ← Form/Settings/*.class.php (one per settings page)
├── postaffiliatepro_Util_CampaignHelper ← Util/CampaignHelper.class.php
└── Shortcode_Affiliate        ← Shortcode/Affiliate.class.php
```

**`postaffiliatepro_Base`** is the shared foundation. It manages:
- The singleton PAP API session (`getApiSession()`) with login, hashed-script detection, and failed-login throttling (locks out after 5 failures in 15 minutes)
- Affiliate and transaction CRUD via PAP API (`changeAffiliateStatus`, `changeOrderStatus`, `loadTransactionsByOrderID`, `fireRecurringCommissions`)
- The `sendRequest()` HTTP wrapper around `wp_safe_remote_get`
- The `_log()` debug logger (writes to a file only when debugging is enabled in settings)

**`postaffiliatepro_Form_Base`** is a simple template engine. `render()` reads a `.xtpl` file and replaces `{variable}` tokens with values built by `initForm()`. It wraps built-in helpers like `addCheckbox()`, `addTextBox()`, `addSelect()`, etc., which all call `addVariable()`.

**Each integration** (`Form/Settings/WooComm.class.php`, `EDD.class.php`, etc.) is a self-contained class that:
1. Extends `postaffiliatepro_Form_Base` to render its admin config page
2. Contains all its WP hook registrations at the bottom of its file (instantiated at file-include time)
3. Has a paired `Template/*.xtpl` file for its UI

## Templates

`Template/*.xtpl` files are plain HTML using `{variable}` token substitution — not a third-party template engine. Tokens are replaced verbatim by `str_replace` in `Form/Base.class.php:render()`. To add a new field to a settings page, add a token to the `.xtpl` and call `addVariable('token', $html)` (or a helper) in `initForm()`.

## Settings storage

All settings are stored in the WordPress options table via `get_option` / `update_option`. Setting name constants are defined as `const` on each form class (e.g., `postaffiliatepro_Form_Settings_WooComm::WOOCOMM_CONFIG_PAGE`). Settings are registered with `register_setting()` inside each class's `initSettings()` method, which is hooked to `admin_init`.

## Tracking code injection

Two mutually exclusive tracking modes are supported:

- **Synchronous** (default): injects a `<script src="…/trackjs.js">` tag, followed by a `<script>` with inline JS.
- **Async**: wraps everything in an `(function(d,t){…})(document,"script")` IIFE. Toggled by `postaffiliatepro::ASYNC_ENABLED` option.

The helpers `getPAPTrackJSDynamicCode()` and `getPAPTrackJSAsyncCode($content)` on the main class produce these snippets. All integrations call one of these two methods to emit tracking HTML.

## Adding a new integration

1. Create `Form/Settings/MyPlugin.class.php` extending `postaffiliatepro_Form_Base`
2. Create `Template/MyPluginConfig.xtpl`
3. Add an image to `resources/img/`
4. Register the integration's enable checkbox in `Form/Settings/Integrations.class.php`
5. Add the `require_once` to `postaffiliatepro::initForms()` in `postaffiliatepro.php`
6. At the bottom of the new class file, instantiate and register hooks (follow the pattern in WooComm, EDD, etc.)

## Security patterns to follow

- All user-supplied values must pass through `sanitize_text_field()`, `esc_url_raw()`, or `wp_unslash()` + `filter_var()` before use
- All output to the page must use `esc_attr()` or `esc_url()` (or be emitted through the form helpers which do this)
- AJAX handlers must use `wp_send_json_success` / `wp_send_json_error` and call `wp_die()` at the end
- The PAP URL field has SSRF protection in `postaffiliatepro::sanitizeUrl()` — it resolves the hostname and blocks private/reserved IP ranges

## WordPress hook registration pattern

Each integration file instantiates its class and calls `add_action`/`add_filter` globally at the bottom of the file. This means hooks are registered as soon as `initForms()` runs the `require_once`. There is no plugin activation hook for feature setup; integrations are always loaded.
