# PayPal v2 Module

The paypalv2 module is the PayPal Orders v2 payment adapter. It keeps provider operations in the PayPal Server SDK wrapper, while all order ownership, pricing, currency, amount, refund, source, and idempotent completion checks stay in the module completion boundary.

## Sources

- src/core/modules/paypalv2/paypalv2.api.php: SDK client, Orders v2, capture, Transaction Search, OAuth fallback, and webhook signature verification.
- src/core/modules/paypalv2/paypalv2.class.php: checkout creation, retry reuse, SDK capture endpoint, return/webhook processing, shared validation, and cron reconciliation.
- src/core/modules/paypalv2/paypalv2.php: public store dispatcher for capture, return, and webhook actions.
- src/core/modules/paypalv2/paypalv2.js: PayPal Web SDK v6, Venmo, Apple Pay, and Google Pay browser sessions. It never polls.
- src/model/base/order-temp-txn.model.php: retains all provider references for retry and callback idempotency.
- src/core/cli/payment.php: external reconciliation command.

## Configuration

The complete annotated block is present in src/settings/config.example.php and should be copied to the deployment configuration. Every setting is described there. The supported values are:

- enabled: exposes the payment method.
- label and image: store presentation.
- prod_mode: selects PayPal live or sandbox endpoints and controls non-admin checkout visibility.
- display_mode: sdk renders Web SDK v6 buttons; new_tab renders only the server-generated approval link and works without JavaScript. SDK mode disables browser polling.
- payment_sources: allowlist containing any of paypal, venmo, apple_pay, and google_pay. Wallet eligibility is still decided by the browser and PayPal.
- merchant_country: ISO country code used for eligibility and Apple or Google wallet requests.
- sdk_locale: optional locale reserved for Web SDK rendering.
- client_id and client_secret: PayPal REST app credentials. Only client_id reaches the browser; client_secret stays server-side.
- webhook_id: PayPal webhook ID used by verify-webhook-signature. Register WEB_URL/store?action=ipn&method=<payment-key>.
- currency_code: fallback currency when the local order does not provide one.
- approveManualPayments: enables the CLI reconciliation flow.
- manualPaymentDescription: text marker containing the order_id placeholder, for example Order #{order_id}. Matching is case-insensitive and allows extra text before, between, or after the marker.
- manualPaymentSearchDays: rolling Transaction Search window, default two days and capped at 31.
- timeout: provider HTTP timeout in seconds.
- polling_interval: retained for compatibility with older configuration; it is not used by SDK mode.
- discount: shared payment discount or fee percentage.

## Server checkout flow

1. PaymentHelpers starts the module with the final calculated order price.
2. The module loads every cms_orders_temp_txn reference for the local order, newest first, and re-queries PayPal before creating another order.
3. Approved provider orders are captured. Pending reusable orders render their approval link. Terminal orders are left for audit and a new provider order is created.
4. Each new PayPal order is created with CAPTURE intent, custom_id and reference_id equal to the local order ID, the configured currency, and a description generated from manualPaymentDescription.
5. The provider order ID is inserted into cms_orders_temp_txn. Existing rows are never overwritten.
6. SDK mode renders a fallback approval link plus eligible SDK buttons. The SDK receives the eager provider order ID and calls the authenticated server capture action after approval.
7. new_tab mode renders only the approval link, so no JavaScript is required.

## SDK flow

The Web SDK v6 core is loaded only in sdk mode. PayPal and Venmo use one-time payment sessions and custom elements. Apple Pay uses the native ApplePaySession plus PayPal merchant validation and confirmOrder. Google Pay uses the Google PaymentsClient, PayPal confirmOrder, and the configured Google Pay component. Each callback submits the local order ID, provider order ID, source, and CSRF token to store?action=capture&method=<payment-key>.

The server authenticates the account, verifies the CSRF token, resolves the order by account ownership, checks the temporary provider reference, validates the configured source, re-queries PayPal, captures approved orders, and runs the same final completion validator used by webhooks and cron. The browser does not decide amount, currency, order status, or fulfillment.

## Webhook flow

Register WEB_URL/store?action=ipn&method=<payment-key> for PAYMENT.CAPTURE.COMPLETED. The module validates PayPal transmission headers and webhook_id through the PayPal Notifications API, re-queries the capture and order, checks local custom_id or reference_id, capture status, currency, amount, funding source, and order state, then calls PaymentModule::completePayment. Unknown temporary references are acknowledged without fulfillment. Duplicate events are safe.

## Return and manual verification

The provider return URL is WEB_URL/store?action=return&method=<payment-key>. The return handler resolves the stored provider order and performs the same server verification. checkPaymentStatus remains available for explicit recovery or legacy callers, but SDK mode does not schedule it from the browser.

## Final validation and idempotency

All successful paths require a verified completed capture or verified Transaction Search record, a matching local order, a configured funding source, matching currency, payment amount at least the calculated PayPal v2 price, and a payable PENDING order. A payment already recorded with the same transaction ID returns success without repeating logging, email, or fulfillment. Refunds, reversals, incomplete captures, mismatched references, disabled sources, expired or non-pending orders, and insufficient payments are rejected or ignored.

## Manual reconciliation

Run:

    php src/cli.php payment approvePaypalV2ManualPayments

The command exits without provider calls unless approveManualPayments is true. It scans every Transaction Search page in the rolling window, sorts successful records before older or unsuccessful records, collects refund and reversal references, matches configured description fields, and only attempts PENDING orders. It accepts a transaction when the currency matches and the gross amount is equal to or greater than the calculated order price. Transaction Search permission is required on the PayPal REST app; PayPal reporting can lag by several hours, which is why the window overlaps between runs.

Operational setup

- Create separate sandbox and live PayPal REST apps and set the matching credentials with prod_mode.
- Register the webhook and copy its ID to webhook_id.
- Enable Transaction Search permissions for the app used by the cron.
- For Venmo, use a supported US configuration.
- For Apple Pay, use HTTPS and complete PayPal merchant-domain and Apple Pay enrollment.
- For Google Pay, enable the googlepay-payments component and Google Pay in the PayPal account.
- Keep client_secret and webhook credentials server-side.
