=== Donation Router for GiveWP & PayPal ===
Contributors: mralaminahamed
Donate link: https://alaminahamed.com/donate
Tags: givewp, paypal, donations, country-routing, charity
Requires at least: 6.4
Tested up to: 6.9
Requires PHP: 8.1
Stable tag: 1.1.0
License: GPL-2.0-or-later
License URI: https://www.gnu.org/licenses/gpl-2.0.html

Route GiveWP donations to multiple PayPal Business accounts by donor country. Multi-entity charities, automatic credential switching.

== Description ==

Donation Router for GiveWP & PayPal extends the GiveWP PayPal Donations gateway so that a single WordPress installation can accept donations through several PayPal Business accounts, automatically routing each transaction to the correct account based on the donor's declared country.

This plugin is an independent, third-party extension. It is not affiliated with, endorsed by, or sponsored by GiveWP (StellarWP) or PayPal. The names "GiveWP" and "PayPal" appear here solely to describe compatibility with those products.

This plugin is designed for organisations that operate two or more legally distinct charity entities — for example a US 501(c)(3) and a Canadian CRA-registered charity — that share a single donation website but must process payments through separate PayPal accounts and issue separate tax receipts.

**Key features:**

* **Multi-account admin panel** — Add and manage any number of PayPal Business accounts under Donation Forms → PayPal Accounts. Each account stores legal entity details (name, tax ID, address, country) alongside AES-256-GCM encrypted API credentials.
* **Country routing rules** — Define which PayPal account and GiveWP form handles donations from each country. One active rule per country, fully configurable from the admin.
* **Country selector popup** — A donor-facing modal popup (shortcode `[ador_country_selector]`) asks donors to identify their country before proceeding to checkout. Selection is stored in sessionStorage to avoid repeated prompts.
* **Runtime credential routing** — The plugin intercepts GiveWP's IoC container at payment time, substituting the correct PayPal credentials transparently. No GiveWP core files are modified.
* **Per-account webhook handling** — Each PayPal account receives its own webhook endpoint (`/wp-json/ador/v1/webhook/{account_id}`) with independent HMAC signature verification.
* **Per-entity email tags** — Seven GiveWP email template tags populate receipt emails with the correct legal entity name, address, tax ID, tax-deductibility statement, and a unique per-entity receipt number.
* **PDF receipts** — Optional on-demand PDF receipt download on the GiveWP success page, generated by dompdf.
* **Security-first design** — AES-256-GCM credential storage, nonce protection, capability checks, webhook signature verification, and `$wpdb->prepare()` throughout.

== Installation ==

1. Upload the `alamin-donation-router` folder to the `/wp-content/plugins/` directory.
2. Run `composer install --no-dev --optimize-autoloader` inside the plugin directory.
3. Ensure `AUTH_KEY` and `SECURE_AUTH_SALT` are set to strong, unique values in `wp-config.php` — these are required for credential encryption.
4. Activate the plugin through the **Plugins** screen in WordPress.
5. Navigate to **Donation Forms → PayPal Accounts** and add your first PayPal Business account.
6. Configure country routing rules under the **Country Routing** tab.
7. Add the shortcode `[ador_country_selector]` to your donation landing page.
8. Register per-account webhook URLs in your PayPal Developer apps (see the **Webhook Setup** view for each account).

For detailed configuration instructions, see [operations.md](https://github.com/mralaminahamed/donation-router-for-givewp-paypal-donations/blob/main/resources/plan/operations.md) in the GitHub repository.

== Frequently Asked Questions ==

= Which payment gateways does this plugin support? =

This plugin supports the GiveWP PayPal Donations gateway (also known as PayPal Commerce / PPCP) only. It does not affect or support other GiveWP gateways such as Stripe, Authorize.net, or PayPal Standard.

= How many PayPal accounts can I add? =

There is no hard limit on the number of PayPal Business accounts you can configure. Each account requires its own PayPal Developer REST app (Client ID and Client Secret) and its own webhook URL registered in PayPal's developer portal.

= How are PayPal credentials stored? Is it safe? =

Yes. PayPal Client IDs and Client Secrets are never stored in plaintext. They are encrypted with AES-256-GCM using a key derived via HKDF-SHA256 (RFC 5869) from your WordPress `AUTH_KEY` and `SECURE_AUTH_SALT` constants (32-byte key). The random nonce and GCM authentication tag are stored alongside the ciphertext as a base64 blob. Credentials are never written to any log. If `AUTH_KEY` or `SECURE_AUTH_SALT` are changed, stored credentials become unreadable and must be re-entered.

= Can I test with PayPal sandbox accounts before going live? =

Yes. The plugin works with both PayPal sandbox and live environments. Add your sandbox accounts the same way as live accounts, using the Client IDs and Secrets from your sandbox REST apps. Register sandbox webhook URLs in PayPal's developer portal using a publicly accessible HTTPS URL (use a tunnelling tool such as ngrok for local development). See [operations.md](https://github.com/mralaminahamed/donation-router-for-givewp-paypal-donations/blob/main/resources/plan/operations.md) for a full sandbox testing procedure.

= What happens if a routing rule is missing for a donor's country? =

If no active routing rule exists for the selected country, the credential router falls back to GiveWP's primary PayPal account. The donation is not blocked — it proceeds normally using the default account. An error entry is written to the PHP error log with prefix `[ADOR]` for the operator's attention.

= Does this plugin modify GiveWP core files? =

No. The plugin hooks into GiveWP exclusively through WordPress filters, actions, and GiveWP's public IoC container (`give()`). No GiveWP core files are modified.

= Can I use this plugin with GiveWP's built-in email receipts? =

Yes. The plugin registers additional GiveWP email template tags that you can insert into your existing GiveWP email templates (GiveWP → Settings → Emails). Tags include `{paypal_account_entity_name}`, `{paypal_account_entity_tax_id}`, `{paypal_account_receipt_number}`, and four others. Tags return an empty string for donations not routed through this plugin, so existing templates work without changes.

= The PDF receipt is not generating. What should I check? =

Verify that `dompdf/dompdf` is installed by running `composer install` in the plugin directory. Confirm that the `ador_pdf_receipts_enabled` option is set to a truthy value in `wp_options`. Check the PHP error log for a line beginning `[ADOR] PDF receipts disabled:` — this will include the underlying error message.

== Screenshots ==

1. Accounts list table — all configured PayPal Business accounts with Active/Inactive status badges, country, legal entity, and action buttons.
2. Add Account form — entering legal entity details and connecting a PayPal Business account via OAuth.
3. Country Routing list table — routing rules mapping donor countries to donation forms and PayPal accounts.
4. Webhook Setup view — per-account webhook URL with one-click copy and the required PayPal event type list.
5. Country selector popup — donor-facing modal that lets donors choose their country/legal entity before checkout.
6. Plugin Settings tab — configuring the country selector popup, geolocation provider, PDF receipts, and legal acknowledgement.
7. Edit Account form — per-account legal entity details and receipt template editor with available GiveWP email tags.

== External services ==

This plugin sends data to PayPal's REST API to authenticate with PayPal Business accounts and to verify the HMAC-SHA256 signatures of inbound webhook events.

**PayPal REST API** (`https://api-m.paypal.com`, `https://api-m.sandbox.paypal.com`, `https://api.paypal.com`, `https://api.sandbox.paypal.com`)

* **What is sent:** PayPal Client ID and Client Secret (decrypted in memory at request time, never stored in plaintext) are exchanged for OAuth 2.0 bearer tokens during donation checkout. Webhook event payloads are forwarded to PayPal's `/v1/notifications/verify-webhook-signature` endpoint for signature verification.
* **When it is sent:** Only when a donation is being processed through a PayPal Business account configured in this plugin, or when PayPal sends a webhook event to this site.
* **Service terms:** [PayPal User Agreement](https://www.paypal.com/us/legalhub/useragreement-full) | [PayPal Privacy Statement](https://www.paypal.com/us/legalhub/privacy-full)

== Changelog ==

= 1.1.0 - 2026-07-06 =

**Added**

* Per-account live/sandbox environment — manage the same PayPal merchant independently in live and sandbox; routing only applies an account whose mode matches the site's current GiveWP mode.
* Import GiveWP's already-connected PayPal account in one click (reuses stored credentials, no second OAuth).
* Re-sync an imported account's credentials from GiveWP (same-merchant, same-mode guarded).
* Re-activate a deactivated account directly from the Accounts list.
* Delete a PayPal account from the Accounts list (confirmation required; deregisters the account's PayPal webhook first).
* GiveWP version compatibility guard — refuses below GiveWP 3.0 and warns on untested GiveWP majors (`ador_block_on_untested_givewp` filter to fail closed).

**Changed**

* Accounts list gains a Mode column (live/sandbox badge).
* The Import button is always shown on the Accounts list for discoverability — disabled with an inline reason when there is nothing to import (GiveWP not connected, or the account is already imported).
* Flat, minimal admin restyle built on the WordPress core colour palette — status/mode badges, buttons, modals, notices, and the receipt templates. Primary calls to action now use WordPress's native primary-button style.
* Schema 1.1.0: adds the `mode` column and a composite `UNIQUE(merchant_id, mode)` constraint.
* Schema now migrates on plugin update, not only on activation.

**Fixed**

* List-table bulk actions now reach their handlers. The bulk **Apply** collided with `WP_List_Table`'s own action field and dispatched to no handler, dying with an empty error page; the routing field is now printed after the table so it wins.
* Refunds to routed accounts no longer fail silently — the PayPal access token is re-minted at refund time instead of reusing the stale connect-time token, and a refund whose account mode does not match the site mode is blocked with an actionable admin error.
* Inbound webhooks for a genuinely unknown account return 404, while a real processing error now logs and returns 500 instead of being masked as not-found.
* Cached PayPal access tokens are encrypted at rest and re-minted on decrypt failure.
* Credential routing no longer fatals against GiveWP's PayPal Commerce contract (overrider parameter-type compatibility).
* Per-account credentials no longer leak to `wp_options` when GiveWP refreshes an access token.
* Inbound PayPal webhooks read the WP-canonicalised transmission headers, fixing signature verification and dropped events.

**Removed**

* The unused `ador_paypal_mode` option.

= 1.0.0 - 2026-05-07 =

**Added**

* Multi-account admin panel (Donation Forms → PayPal Accounts) with Accounts, Country Routing, and Settings tabs.
* Account add/edit form with AES-256-GCM encrypted credential storage.
* Country routing rules with add/edit/bulk-deactivate actions.
* Custom database tables `{prefix}ador_accounts` and `{prefix}ador_country_routing` with schema versioning.
* `PayPal_Credential_Router` intercepting GiveWP's IoC container before every PayPal AJAX payment request to substitute per-account credentials.
* IoC container rebinding for both `MerchantDetail::class` and `MerchantDetails::class` covering all GiveWP PayPal credential resolution paths.
* Refund routing with inactive-account guard.
* Per-account REST webhook endpoint `POST /wp-json/ador/v1/webhook/{account_id}` with HMAC-SHA256 signature verification and idempotency checking.
* Country selector popup shortcode `[ador_country_selector]` with sessionStorage persistence.
* Geolocation hint (non-binding IP-based country pre-selection).
* Seven GiveWP email template tags for per-entity receipt details.
* Per-entity sequential receipt numbers allocated atomically via `FOR UPDATE` lock.
* Default tax-deductibility statement templates for US and Canada.
* PDF receipt generation via dompdf behind `ador_pdf_receipts_enabled` option flag.
* Action hooks `ador_credentials_routed` and `ador_webhook_processed`.
* PHPStan level 8, WPCS 3.x, PHPUnit 9.6 with Brain\Monkey test suite.

**Security**

* AES-256-GCM encryption for all stored PayPal credentials with HKDF-SHA256 key derivation.
* PayPal HMAC-SHA256 webhook signature verification on all inbound webhook events.
* WordPress nonce protection on all admin form submissions and AJAX endpoints.
* `current_user_can( 'manage_give_settings' )` capability checks on all admin-only actions.
* `$wpdb->prepare()` for all parameterised database queries.
