Skip to content

Commit 03bdae6

Browse files
authored
t523: feat(paypal): PayPal PPCP integration review compliance (#726)
- Update disconnect dialog to use PayPal's required disclaimer text - Show error banners when payments_receivable=false or email_confirmed=false after OAuth - Block paypal-rest from active gateways when merchant status is invalid - Add payee.merchant_id to purchase_units when connected via OAuth - Log PayPal-Debug-Id header from every API response Closes #725
1 parent c687d32 commit 03bdae6

2 files changed

Lines changed: 90 additions & 31 deletions

File tree

inc/gateways/class-paypal-oauth-handler.php

Lines changed: 19 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -260,23 +260,34 @@ public function handle_oauth_return(): void {
260260
wu_save_setting('paypal_rest_connection_mode', $mode_prefix);
261261

262262
// Store additional status info if available
263-
if (! empty($merchant_status['paymentsReceivable'])) {
264-
wu_save_setting("paypal_rest_{$mode_prefix}_payments_receivable", $merchant_status['paymentsReceivable']);
265-
}
263+
$payments_receivable = isset($merchant_status['paymentsReceivable']) ? (bool) $merchant_status['paymentsReceivable'] : true;
264+
$email_confirmed = isset($merchant_status['emailConfirmed']) ? (bool) $merchant_status['emailConfirmed'] : true;
266265

267-
if (! empty($merchant_status['emailConfirmed'])) {
268-
wu_save_setting("paypal_rest_{$mode_prefix}_email_confirmed", $merchant_status['emailConfirmed']);
269-
}
266+
wu_save_setting("paypal_rest_{$mode_prefix}_payments_receivable", $payments_receivable);
267+
wu_save_setting("paypal_rest_{$mode_prefix}_email_confirmed", $email_confirmed);
270268

271269
// Clean up the tracking transient
272270
delete_site_transient('wu_paypal_onboarding_' . $tracking_id);
273271

274-
wu_log_add('paypal', sprintf('PayPal OAuth completed. Merchant ID: %s, Mode: %s', $merchant_id, $mode_prefix));
272+
wu_log_add('paypal', sprintf('PayPal OAuth completed. Merchant ID: %s, Mode: %s, payments_receivable: %s, email_confirmed: %s', $merchant_id, $mode_prefix, $payments_receivable ? 'true' : 'false', $email_confirmed ? 'true' : 'false'));
275273

276274
// Automatically install webhooks for the connected account
277275
$this->install_webhook_after_oauth($mode_prefix);
278276

279-
$this->add_oauth_notice('success', __('PayPal account connected successfully!', 'ultimate-multisite'));
277+
// Show required PayPal error messages when merchant status is incomplete
278+
if (! $payments_receivable) {
279+
$this->add_oauth_notice(
280+
'error',
281+
__('Your PayPal account is not yet able to receive payments. Please complete your PayPal account setup and try connecting again.', 'ultimate-multisite')
282+
);
283+
} elseif (! $email_confirmed) {
284+
$this->add_oauth_notice(
285+
'error',
286+
__('Your PayPal account email address is not confirmed. Please confirm your email with PayPal and try connecting again.', 'ultimate-multisite')
287+
);
288+
} else {
289+
$this->add_oauth_notice('success', __('PayPal account connected successfully!', 'ultimate-multisite'));
290+
}
280291

281292
// Redirect to remove query parameters
282293
wp_safe_redirect(

inc/gateways/class-paypal-rest-gateway.php

Lines changed: 71 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -193,6 +193,9 @@ public function hooks(): void {
193193
// Hide PayPal from checkout when currency is not supported
194194
add_filter('wu_get_active_gateways', [$this, 'maybe_remove_for_unsupported_currency']);
195195

196+
// Hide PayPal from checkout when merchant cannot receive payments
197+
add_filter('wu_get_active_gateways', [$this, 'maybe_remove_for_invalid_merchant_status']);
198+
196199
// Register PayPal checkout scripts (button branding)
197200
add_action('wu_checkout_scripts', [$this, 'register_scripts']);
198201

@@ -246,6 +249,37 @@ public function maybe_remove_for_unsupported_currency(array $gateways): array {
246249
return $gateways;
247250
}
248251

252+
/**
253+
* Removes PayPal from the active gateways list when the merchant cannot receive payments.
254+
*
255+
* PayPal requires that merchants with `payments_receivable=false` or
256+
* `email_confirmed=false` are blocked from processing payments until
257+
* their account setup is complete.
258+
*
259+
* Hooked to 'wu_get_active_gateways'.
260+
*
261+
* @since 2.0.0
262+
* @param array $gateways The registered active gateways.
263+
* @return array
264+
*/
265+
public function maybe_remove_for_invalid_merchant_status(array $gateways): array {
266+
267+
// Only applies when connected via OAuth
268+
if (empty($this->merchant_id)) {
269+
return $gateways;
270+
}
271+
272+
$mode_prefix = $this->test_mode ? 'sandbox' : 'live';
273+
$payments_receivable = wu_get_setting("paypal_rest_{$mode_prefix}_payments_receivable", true);
274+
$email_confirmed = wu_get_setting("paypal_rest_{$mode_prefix}_email_confirmed", true);
275+
276+
if (! $payments_receivable || ! $email_confirmed) {
277+
unset($gateways['paypal-rest']);
278+
}
279+
280+
return $gateways;
281+
}
282+
249283
/**
250284
* Returns a branded HTML label for the PayPal option in the checkout gateway selector.
251285
*
@@ -678,6 +712,12 @@ protected function api_request(string $endpoint, array $data = [], string $metho
678712
return $response;
679713
}
680714

715+
// Log PayPal-Debug-Id for every response to aid support and review submissions.
716+
$debug_id = wp_remote_retrieve_header($response, 'paypal-debug-id');
717+
if ($debug_id) {
718+
$this->log(sprintf('PayPal-Debug-Id: %s [%s %s]', $debug_id, $method, $endpoint));
719+
}
720+
681721
$body = json_decode(wp_remote_retrieve_body($response), true);
682722
$code = wp_remote_retrieve_response_code($response);
683723

@@ -1010,6 +1050,35 @@ protected function create_order($payment, $membership, $customer, $cart, $type):
10101050
$currency = $this->get_payment_currency_code($payment);
10111051
$description = $this->get_subscription_description($cart);
10121052

1053+
$purchase_unit = [
1054+
'reference_id' => $payment->get_hash(),
1055+
'description' => substr($description, 0, 127),
1056+
'custom_id' => sprintf('%s|%s|%s', $payment->get_id(), $membership->get_id(), $customer->get_id()),
1057+
'amount' => [
1058+
'currency_code' => $currency,
1059+
'value' => $this->format_amount($payment->get_total(), $currency),
1060+
'breakdown' => [
1061+
'item_total' => [
1062+
'currency_code' => $currency,
1063+
'value' => $this->format_amount($payment->get_subtotal(), $currency),
1064+
],
1065+
'tax_total' => [
1066+
'currency_code' => $currency,
1067+
'value' => $this->format_amount($payment->get_tax_total(), $currency),
1068+
],
1069+
],
1070+
],
1071+
'items' => $this->build_order_items($cart, $currency),
1072+
];
1073+
1074+
// Include payee.merchant_id when connected via OAuth so PayPal routes
1075+
// the payment to the correct merchant account (required for PPCP compliance).
1076+
if (! empty($this->merchant_id)) {
1077+
$purchase_unit['payee'] = [
1078+
'merchant_id' => $this->merchant_id,
1079+
];
1080+
}
1081+
10131082
$order_data = [
10141083
'intent' => 'CAPTURE',
10151084
'payment_source' => [
@@ -1025,28 +1094,7 @@ protected function create_order($payment, $membership, $customer, $cart, $type):
10251094
'email_address' => $customer->get_email_address(),
10261095
],
10271096
],
1028-
'purchase_units' => [
1029-
[
1030-
'reference_id' => $payment->get_hash(),
1031-
'description' => substr($description, 0, 127),
1032-
'custom_id' => sprintf('%s|%s|%s', $payment->get_id(), $membership->get_id(), $customer->get_id()),
1033-
'amount' => [
1034-
'currency_code' => $currency,
1035-
'value' => $this->format_amount($payment->get_total(), $currency),
1036-
'breakdown' => [
1037-
'item_total' => [
1038-
'currency_code' => $currency,
1039-
'value' => $this->format_amount($payment->get_subtotal(), $currency),
1040-
],
1041-
'tax_total' => [
1042-
'currency_code' => $currency,
1043-
'value' => $this->format_amount($payment->get_tax_total(), $currency),
1044-
],
1045-
],
1046-
],
1047-
'items' => $this->build_order_items($cart, $currency),
1048-
],
1049-
],
1097+
'purchase_units' => [ $purchase_unit ],
10501098
];
10511099

10521100
/**
@@ -1890,7 +1938,7 @@ function () use ($nonce, $sandbox) {
18901938

18911939
$(".wu-paypal-disconnect").on("click", function(e) {
18921940
e.preventDefault();
1893-
if (!confirm(<?php echo wp_json_encode(__('Are you sure you want to disconnect PayPal?', 'ultimate-multisite')); ?>)) return;
1941+
if (!confirm(<?php echo wp_json_encode(__('Disconnecting your PayPal account will prevent you from offering PayPal services and products on your website. Do you wish to continue?', 'ultimate-multisite')); ?>)) return;
18941942

18951943
var $btn = $(this);
18961944
$btn.prop("disabled", true);

0 commit comments

Comments
 (0)