# PayPal Module

PayPal payment request and IPN webhook module.

Sources: `src/core/modules/paypal/paypal.class.php`, `src/core/modules/paypal/paypal.php`

Read [Payment Discounts And Cart Payments](../../../knowledge/21-payment-discounts.md) for the shared pricing and callback contract before changing this adapter.

## Class

| Item | Extends | Description |
|---|---|---|
| `PaypalPayment` | `PaymentModule` | PayPal payment adapter |

## Methods

| Method | Inputs | Description |
|---|---|---|
| `__construct` | none | Initializes PayPal API |
| `getPaymentName` | none | Returns `paypal` |
| `isProdMode` | none | Returns the selected config entry's `prod_mode` |
| `setRequestInfo` | `$accountModel`, `$order`, `$newPrice` | Fills PayPal fields |
| `createRequest` | `$accountModel`, `$order`, `$newPrice` | Posts PayPal form |
| `validatePayment` | none | Validates IPN payload |
| `mapPayPalDataToLog` | `$paypalData` | Maps payment log data |

## Request Fields

| Field | Value |
|---|---|
| `business` | Selected `PAYMENT_MODULES[<payment-key>]['business']` |
| `currency_code` | Selected `PAYMENT_MODULES[<payment-key>]['currency_code']` |
| `amount` | `$newPrice` |
| `item_number` | `$order['order_id']` |
| `quantity` | `1` |
| `invoice` | `$order['order_id'].'_'.time()` |
| `custom` | `$accountModel['Username']` |
| `return` | `purchase-history?orderId=<order_id>` |
| `cancel_return` | `purchase-history?orderId=<order_id>` |
| `notify_url` | `store?action=ipn&method=<payment-key>` |
| `item_name` | `Order #<order_id>` |
| `cpp_logo_image` | `WEB_URL.'assets/logo.png'` |
| `no_note` | `1` |
| `no_shipping` | `1` |
| `address_override` | `1` |
| `lc` | `ES` |

## Webhook

| Item | Value |
|---|---|
| Public invoker | `src/pages/public/store/store.controller.php` |
| Trigger query | `?action=ipn&method=<payment-key>` |
| Module file | `src/core/modules/paypal/paypal.php` |
| Handler | `PaypalPayment::validatePayment()` |
| Exit behavior | exits after handling |

## Webhook Flow

1. PayPal calls `store?action=ipn&method=<payment-key>`.
2. `store.controller.php` detects `action` and `method`.
3. The controller loads `src/core/modules/paypal/paypal.php`.
4. `paypal.php` loads `PaymentModule`, `apiPaypal.php`, and `paypal.class.php`.
5. `paypal.php` creates `PaypalPayment`.
6. `PaypalPayment::validatePayment()` reads `$_POST`.
7. The order is the authority for owner and original total.
8. Existing transaction IDs are checked with `StoreModel::alreadyPaid`.
9. `$api->validate_ipn()` confirms the callback with PayPal.
10. `mapPayPalDataToLog` prepares `cms_payments` data.
11. `PAYPAL_STATUS_SUCCESS` is checked against `$_POST['payment_status']`.
12. `PaymentModule::completePayment()` recalculates and validates the discounted amount, logs the audit row, updates the order, fulfills it, and sends the first-payment email.
13. `saveLog('data_received', ...)` stores request data.
14. `paypal.php` exits after the webhook.

## Request Flow

1. The store page calls `StartPayment`.
2. `StartPayment` loads `paypal/paypal.php`.
3. `PaypalPayment::__construct` creates `paypal_class`.
4. Merchant and currency fields are added.
5. `PaymentHelpers::startPayment()` calculates `newPrice`.
6. `createRequest` calls `setRequestInfo` with the order and `newPrice`.
7. `setRequestInfo` fills amount, order, invoice, and account.
8. Return, cancel, and IPN URLs are added.
9. `dump_fields_plain` output is logged as `data_sent`.
10. `submit_paypal_post` prints the PayPal form.
11. `createRequest` exits to stop page rendering.

## Config

Source: `src/settings/config.developer.php`

| Key | Description |
|---|---|
| `PAYMENT_MODULES['paypal']['enabled']` | Enables module |
| `PAYMENT_MODULES['paypal']['label']` | Store button label |
| `PAYMENT_MODULES['paypal']['discount']` | Positive discount or negative fee percentage |
| `PAYMENT_MODULES['paypal']['prod_mode']` | Live mode flag |
| `PAYMENT_MODULES['paypal']['business']` | PayPal merchant |
| `PAYMENT_MODULES['paypal']['currency_code']` | Payment currency |

## Constants

| Constant | Value |
|---|---|
| `paypal_enabled` | `PAYMENT_MODULES['paypal']['enabled']` |
| `paypal_prod_mode` | `PAYMENT_MODULES['paypal']['prod_mode']` |
| `paypal_business` | `PAYMENT_MODULES['paypal']['business']` |
| `paypal_currency_code` | `PAYMENT_MODULES['paypal']['currency_code']` |
| `paypal_url` | sandbox or live URL |

## Notes

| Item | Note |
|---|---|
| API library | Wrapped by `paypal.class.php` |
| Request log | `data_sent/<username>.log` |
| Webhook log | `data_received/<username>.log` |
| Success status | `PAYPAL_STATUS_SUCCESS` |
| Payment row | Logged by `PaymentModule::completePayment()` after valid successful IPN |
| Order row | Stores original total, final total, percentage, payment method, and transaction ID |
| Purchase rows | Fulfilled only after the order is marked paid |

## Tips

1. Keep `custom` as account identity.
2. Keep `item_number` as the local order ID.
3. Keep `txn_id` idempotency checks.
4. Review the shared `completePayment()` amount comparison before refactors.
5. Preserve webhook exit behavior.
