← All changes
|
src/includes/classes/paypal-checkout-in.inc.php
+142
-26
260913
→
261001
View file →
| @@ -76,8 +76,11 @@ | ||
| 76 | 76 | { |
| 77 | 77 | echo wp_json_encode(array('error' => 'invalid_token')); |
| 78 | 78 | exit(); |
| 79 | 79 | } |
| 80 | + //260928.1645 Never write a reusable signed Gateway Checkout browser token to PayPal debug logs; the encrypted shortcode token remains available to the handler itself. | |
| 81 | + $log_token = $token; | |
| 82 | + unset($log_token['gateway_checkout_token']); | |
| 80 | 83 | if(!empty($token['exp']) && is_numeric($token['exp']) && time() > (int)$token['exp']) |
| 81 | 84 | { |
| 82 | 85 | echo wp_json_encode(array('error' => 'token_expired')); |
| 83 | 86 | exit(); |
| @@ -92,8 +95,24 @@ | ||
| 92 | 95 | echo wp_json_encode(array('error' => 'token_checksum_mismatch')); |
| 93 | 96 | exit(); |
| 94 | 97 | } |
| 95 | 98 | |
| 99 | + //260928.1520 Framework button shortcodes use the same durable coordinator as Pro-Forms, initialized before any provider-side create operation and required for later browser return/confirmation. | |
| 100 | + if(strpos((string)$token['invoice'], 's2mb-') === 0 && $op !== 'cancel') | |
| 101 | + { | |
| 102 | + $create_allowed = in_array($op, array('create_order', 'create_subscription', 'get_plan_id', 'redirect'), TRUE); | |
| 103 | + $prepared = c_ws_plugin__s2member_paypal_utilities::paypal_checkout_button_gateway_checkout_prepare($token, $create_allowed); | |
| 104 | + if(empty($prepared['ok'])) | |
| 105 | + { | |
| 106 | + $error = !empty($prepared['error']) ? (string)$prepared['error'] : 'gateway_checkout_unavailable'; | |
| 107 | + if($is_redirect_mode) | |
| 108 | + echo esc_html($error); | |
| 109 | + else | |
| 110 | + echo wp_json_encode(array('error' => $error)); | |
| 111 | + exit(); | |
| 112 | + } | |
| 113 | + } | |
| 114 | + | |
| 96 | 115 | if($token['ip'] !== c_ws_plugin__s2member_utils_ip::current()) |
| 97 | 116 | { |
| 98 | 117 | //260414 PayPal Checkout browser returns can legitimately arrive with a different client IP; log it, but do not fail the token. |
| 99 | 118 | c_ws_plugin__s2member_utils_logs::log_entry('paypal-checkout', array( |
| @@ -99,9 +118,9 @@ | ||
| 99 | 118 | c_ws_plugin__s2member_utils_logs::log_entry('paypal-checkout', array( |
| 100 | 119 | 'ppco' => 'checkout', |
| 101 | 120 | 'env_setting' => $env_setting, |
| 102 | 121 | 'event' => 'token_ip_mismatch', |
| 103 | - 'token' => $token, | |
| 122 | + 'token' => $log_token, | |
| 104 | 123 | 'ip' => c_ws_plugin__s2member_utils_ip::current(), |
| 105 | 124 | )); |
| 106 | 125 | } |
| 107 | 126 | |
| @@ -143,9 +162,9 @@ | ||
| 143 | 162 | 'ppco' => 'checkout', |
| 144 | 163 | 'env_setting' => $env_setting, |
| 145 | 164 | 'event' => 'redirect_order_create_response', |
| 146 | 165 | 'order' => $order, |
| 147 | - 'token' => $token, | |
| 166 | + 'token' => $log_token, | |
| 148 | 167 | )); |
| 149 | 168 | |
| 150 | 169 | $approve_url = ''; |
| 151 | 170 | if(!empty($order['links']) && is_array($order['links'])) |
| @@ -156,8 +175,18 @@ | ||
| 156 | 175 | if($rel === 'approve' || $rel === 'payer-action' || $rel === 'approval_url') |
| 157 | 176 | $approve_url = (string)$link['href']; |
| 158 | 177 | } |
| 159 | 178 | |
| 179 | + //260928.1605 A resumed Gateway Checkout returns its existing provider ID, not the original create response links; fetch the provider resource rather than create another chargeable order. | |
| 180 | + if(!$approve_url && !empty($order['id']) && strpos((string)$token['invoice'], 's2mb-') === 0) | |
| 181 | + { | |
| 182 | + $order_details = c_ws_plugin__s2member_paypal_utilities::paypal_checkout_order_details((string)$order['id']); | |
| 183 | + if(empty($order_details['__error']) && !empty($order_details['links']) && is_array($order_details['links'])) | |
| 184 | + foreach($order_details['links'] as $link) | |
| 185 | + if(!empty($link['rel']) && !empty($link['href']) && in_array(strtolower((string)$link['rel']), array('approve', 'payer-action', 'approval_url'), TRUE)) | |
| 186 | + $approve_url = (string)$link['href']; | |
| 187 | + } | |
| 188 | + | |
| 160 | 189 | if(!$approve_url) |
| 161 | 190 | { |
| 162 | 191 | echo 'order_approval_url_missing'; |
| 163 | 192 | exit(); |
| @@ -174,9 +203,9 @@ | ||
| 174 | 203 | 'ppco' => 'checkout', |
| 175 | 204 | 'env_setting' => $env_setting, |
| 176 | 205 | 'event' => 'redirect_subscription_create_response', |
| 177 | 206 | 'subscription' => $subscription, |
| 178 | - 'token' => $token, | |
| 207 | + 'token' => $log_token, | |
| 179 | 208 | )); |
| 180 | 209 | |
| 181 | 210 | $approve_url = ''; |
| 182 | 211 | if(!empty($subscription['links']) && is_array($subscription['links'])) |
| @@ -183,8 +212,18 @@ | ||
| 183 | 212 | foreach($subscription['links'] as $link) |
| 184 | 213 | if(!empty($link['rel']) && !empty($link['href']) && strtolower((string)$link['rel']) === 'approve') |
| 185 | 214 | $approve_url = (string)$link['href']; |
| 186 | 215 | |
| 216 | + //260928.1605 Reuse the same persisted PayPal subscription on repeated redirect clicks. A details GET may supply the approval link without starting a second subscription. | |
| 217 | + if(!$approve_url && !empty($subscription['id']) && strpos((string)$token['invoice'], 's2mb-') === 0) | |
| 218 | + { | |
| 219 | + $subscription_details = c_ws_plugin__s2member_paypal_utilities::paypal_checkout_subscription_details((string)$subscription['id']); | |
| 220 | + if(empty($subscription_details['__error']) && !empty($subscription_details['links']) && is_array($subscription_details['links'])) | |
| 221 | + foreach($subscription_details['links'] as $link) | |
| 222 | + if(!empty($link['rel']) && !empty($link['href']) && strtolower((string)$link['rel']) === 'approve') | |
| 223 | + $approve_url = (string)$link['href']; | |
| 224 | + } | |
| 225 | + | |
| 187 | 226 | if(!$approve_url) |
| 188 | 227 | { |
| 189 | 228 | echo 'subscription_approval_url_missing'; |
| 190 | 229 | exit(); |
| @@ -218,9 +257,9 @@ | ||
| 218 | 257 | 'amount' => !empty($cap0['amount']['value']) ? (string)$cap0['amount']['value'] : '', |
| 219 | 258 | 'cc' => !empty($cap0['amount']['currency_code']) ? (string)$cap0['amount']['currency_code'] : '', |
| 220 | 259 | 'payer' => !empty($capture['payer']['email_address']) ? (string)$capture['payer']['email_address'] : '', |
| 221 | 260 | 'capture' => $capture, |
| 222 | - 'token' => $token, | |
| 261 | + 'token' => $log_token, | |
| 223 | 262 | )); |
| 224 | 263 | |
| 225 | 264 | if(!empty($capture['__error'])) |
| 226 | 265 | { |
| @@ -233,8 +272,25 @@ | ||
| 233 | 272 | echo 'order_capture_failed'; |
| 234 | 273 | exit(); |
| 235 | 274 | } |
| 236 | 275 | |
| 276 | + //260928.1538 Framework button redirect purchases use the shared coordinator fulfillment instead of a second hand-written notify/return path. | |
| 277 | + if(strpos((string)$token['invoice'], 's2mb-') === 0) | |
| 278 | + { | |
| 279 | + $fulfillment = c_ws_plugin__s2member_paypal_utilities::paypal_checkout_order_fulfill($capture, $token); | |
| 280 | + if(empty($fulfillment['ok']) || empty($fulfillment['rtn_url']) || empty($fulfillment['rtn_post'])) | |
| 281 | + { | |
| 282 | + echo esc_html(!empty($fulfillment['error']) ? (string)$fulfillment['error'] : 'order_fulfillment_failed'); | |
| 283 | + exit(); | |
| 284 | + } | |
| 285 | + echo '<!DOCTYPE html><html><head><meta charset="utf-8" /><meta name="robots" content="noindex,nofollow" /></head><body>'; | |
| 286 | + echo '<form id="s2m_ppco_rtn" method="post" accept-charset="UTF-8" action="'.esc_attr($fulfillment['rtn_url']).'">'; | |
| 287 | + foreach($fulfillment['rtn_post'] as $k => $v) | |
| 288 | + echo '<input type="hidden" name="'.esc_attr($k).'" value="'.esc_attr((string)$v).'" />'; | |
| 289 | + echo '</form><script type="text/javascript">document.getElementById("s2m_ppco_rtn").submit();</script></body></html>'; | |
| 290 | + exit(); | |
| 291 | + } | |
| 292 | + | |
| 237 | 293 | //260818.0126 Keep submitted Pro-Form contact details for pro-emails; they may differ from the payer's PayPal profile. |
| 238 | 294 | $is_pro_form = (!empty($token['s2member_paypal_proxy_use']) && (string)$token['s2member_paypal_proxy_use'] === 'pro-emails'); |
| 239 | 295 | $payer_email = ($is_pro_form && isset($token['payer_email'])) ? sanitize_email((string)$token['payer_email']) : (!empty($capture['payer']['email_address']) ? (string)$capture['payer']['email_address'] : ''); |
| 240 | 296 | $first_name = ($is_pro_form && isset($token['first_name'])) ? (string)$token['first_name'] : (!empty($capture['payer']['name']['given_name']) ? (string)$capture['payer']['name']['given_name'] : ''); |
| @@ -365,9 +421,9 @@ | ||
| 365 | 421 | 'event' => 'subscription_get_response', |
| 366 | 422 | 'subscription_id' => $subscription_id, |
| 367 | 423 | 'code' => !empty($subscription_r['code']) ? (int)$subscription_r['code'] : 0, |
| 368 | 424 | 'body' => !empty($subscription_r['body']) ? (string)$subscription_r['body'] : '', |
| 369 | - 'token' => $token, | |
| 425 | + 'token' => $log_token, | |
| 370 | 426 | )); |
| 371 | 427 | |
| 372 | 428 | $subscription_code = !empty($subscription_r['code']) ? (int)$subscription_r['code'] : 0; |
| 373 | 429 | $subscription_body = !empty($subscription_r['body']) ? (string)$subscription_r['body'] : ''; |
| @@ -391,8 +447,41 @@ | ||
| 391 | 447 | echo 'subscription_custom_id_mismatch'; |
| 392 | 448 | exit(); |
| 393 | 449 | } |
| 394 | 450 | |
| 451 | + //260928.1538 A returned Framework button and an ACTIVATED webhook resolve the same provider subscription against the same saved purchase token. | |
| 452 | + if(strpos((string)$token['invoice'], 's2mb-') === 0) | |
| 453 | + { | |
| 454 | + $fulfillment = c_ws_plugin__s2member_paypal_utilities::paypal_checkout_button_subscription_fulfill($subscription, $token, 'return'); | |
| 455 | + if(!empty($fulfillment['pending_activation'])) | |
| 456 | + { | |
| 457 | + //260928.1703 PayPal can redirect before ACTIVE (or while the webhook holds the checkout lock). Recheck the same signed return, without creating another subscription or showing a false payment failure. | |
| 458 | + $wait_attempt = isset($_GET['s2member_paypal_checkout_wait']) ? max(0, (int)$_GET['s2member_paypal_checkout_wait']) : 0; | |
| 459 | + echo '<!DOCTYPE html><html><head><meta charset="utf-8" /><meta name="robots" content="noindex,nofollow" /><title>PayPal subscription confirmation</title></head><body>'; | |
| 460 | + echo '<p>PayPal is confirming your subscription. Please do not start a second checkout.</p>'; | |
| 461 | + if($wait_attempt < 12) | |
| 462 | + { | |
| 463 | + $poll_url = add_query_arg(array('subscription_id' => $subscription_id, 's2member_paypal_checkout_wait' => $wait_attempt + 1), $return_url); | |
| 464 | + echo '<script type="text/javascript">setTimeout(function(){window.location.replace('.wp_json_encode($poll_url, JSON_HEX_TAG | JSON_HEX_AMP | JSON_HEX_APOS | JSON_HEX_QUOT).');},2000);</script>'; | |
| 465 | + } | |
| 466 | + else | |
| 467 | + echo '<p>Confirmation is taking longer than expected. If PayPal activates the subscription, s2Member will complete it through the webhook; check your registration email before trying again.</p>'; | |
| 468 | + echo '</body></html>'; | |
| 469 | + exit(); | |
| 470 | + } | |
| 471 | + if(empty($fulfillment['ok']) || empty($fulfillment['rtn_url']) || empty($fulfillment['rtn_post'])) | |
| 472 | + { | |
| 473 | + echo esc_html(!empty($fulfillment['error']) ? (string)$fulfillment['error'] : 'subscription_fulfillment_failed'); | |
| 474 | + exit(); | |
| 475 | + } | |
| 476 | + echo '<!DOCTYPE html><html><head><meta charset="utf-8" /><meta name="robots" content="noindex,nofollow" /></head><body>'; | |
| 477 | + echo '<form id="s2m_ppco_rtn" method="post" accept-charset="UTF-8" action="'.esc_attr($fulfillment['rtn_url']).'">'; | |
| 478 | + foreach($fulfillment['rtn_post'] as $k => $v) | |
| 479 | + echo '<input type="hidden" name="'.esc_attr($k).'" value="'.esc_attr((string)$v).'" />'; | |
| 480 | + echo '</form><script type="text/javascript">document.getElementById("s2m_ppco_rtn").submit();</script></body></html>'; | |
| 481 | + exit(); | |
| 482 | + } | |
| 483 | + | |
| 395 | 484 | $subscriber_email = !empty($subscription['subscriber']['email_address']) ? (string)$subscription['subscriber']['email_address'] : ''; |
| 396 | 485 | $first_name = !empty($subscription['subscriber']['name']['given_name']) ? (string)$subscription['subscriber']['name']['given_name'] : ''; |
| 397 | 486 | $last_name = !empty($subscription['subscriber']['name']['surname']) ? (string)$subscription['subscriber']['name']['surname'] : ''; |
| 398 | 487 | |
| @@ -486,9 +575,9 @@ | ||
| 486 | 575 | 'ppco' => 'checkout', |
| 487 | 576 | 'env_setting' => $env_setting, |
| 488 | 577 | 'event' => 'create_subscription_response', |
| 489 | 578 | 'subscription' => $subscription, |
| 490 | - 'token' => $token, | |
| 579 | + 'token' => $log_token, | |
| 491 | 580 | )); |
| 492 | 581 | |
| 493 | 582 | if(empty($subscription['id'])) |
| 494 | 583 | { |
| @@ -512,9 +601,9 @@ | ||
| 512 | 601 | exit(); |
| 513 | 602 | } |
| 514 | 603 | |
| 515 | 604 | $gateway_checkout_id = !empty($token['gateway_checkout_id']) && c_ws_plugin__s2member_gateway_checkouts::valid_id((string)$token['gateway_checkout_id']) ? (string)$token['gateway_checkout_id'] : ''; |
| 516 | - $gateway_checkout = $gateway_checkout_id ? c_ws_plugin__s2member_gateway_checkouts::get($gateway_checkout_id) : FALSE; | |
| 605 | + $gateway_checkout = $gateway_checkout_id ? c_ws_plugin__s2member_gateway_checkouts::load_state($gateway_checkout_id) : FALSE; | |
| 517 | 606 | if(!$gateway_checkout || (string)$gateway_checkout['gateway'] !== 'paypal_checkout' || (string)$gateway_checkout['operation'] !== 'subscription') |
| 518 | 607 | { |
| 519 | 608 | echo wp_json_encode(array('error' => 'gateway_checkout_invalid')); |
| 520 | 609 | exit(); |
| @@ -544,9 +633,9 @@ | ||
| 544 | 633 | 'ppco' => 'checkout', |
| 545 | 634 | 'env_setting' => $env_setting, |
| 546 | 635 | 'event' => 'get_plan_id_response', |
| 547 | 636 | 'plan_id' => $plan_id, |
| 548 | - 'token' => $token, | |
| 637 | + 'token' => $log_token, | |
| 549 | 638 | )); |
| 550 | 639 | |
| 551 | 640 | if(!$plan_id) |
| 552 | 641 | { |
| @@ -575,9 +664,9 @@ | ||
| 575 | 664 | |
| 576 | 665 | $gateway_checkout_id = !empty($token['gateway_checkout_id']) && c_ws_plugin__s2member_gateway_checkouts::valid_id((string)$token['gateway_checkout_id']) ? (string)$token['gateway_checkout_id'] : ''; |
| 577 | 666 | if($gateway_checkout_id) |
| 578 | 667 | { |
| 579 | - $gateway_checkout = c_ws_plugin__s2member_gateway_checkouts::get($gateway_checkout_id); | |
| 668 | + $gateway_checkout = c_ws_plugin__s2member_gateway_checkouts::load_state($gateway_checkout_id); | |
| 580 | 669 | $expected_subscription_id = $gateway_checkout && !empty($gateway_checkout['gateway_ids']['subscription_id']) ? (string)$gateway_checkout['gateway_ids']['subscription_id'] : ''; |
| 581 | 670 | //260901.2145 A coordinator-backed browser may confirm only the PayPal subscription that s2Member created and persisted for this logical checkout. |
| 582 | 671 | if(!$expected_subscription_id || !hash_equals($expected_subscription_id, $subscription_id)) |
| 583 | 672 | { |
| @@ -593,9 +682,9 @@ | ||
| 593 | 682 | 'env_setting' => $env_setting, |
| 594 | 683 | 'event' => 'subscription_get_response', |
| 595 | 684 | 'subscription_id' => $subscription_id, |
| 596 | 685 | 'subscription' => $subscription_r, |
| 597 | - 'token' => $token, | |
| 686 | + 'token' => $log_token, | |
| 598 | 687 | )); |
| 599 | 688 | |
| 600 | 689 | $subscription_code = !empty($subscription_r['code']) ? (int)$subscription_r['code'] : 0; |
| 601 | 690 | $subscription_body = !empty($subscription_r['body']) ? (string)$subscription_r['body'] : ''; |
| @@ -682,8 +771,26 @@ | ||
| 682 | 771 | echo wp_json_encode(array('error' => 'subscription_custom_id_mismatch')); |
| 683 | 772 | exit(); |
| 684 | 773 | } |
| 685 | 774 | |
| 775 | + //260928.1538 Framework button and webhook share a single durable fulfillment path; do not create a second simulated IPN here. | |
| 776 | + if(strpos((string)$token['invoice'], 's2mb-') === 0) | |
| 777 | + { | |
| 778 | + $fulfillment = c_ws_plugin__s2member_paypal_utilities::paypal_checkout_button_subscription_fulfill($subscription, $token, 'browser'); | |
| 779 | + if(!empty($fulfillment['pending_activation'])) | |
| 780 | + { | |
| 781 | + echo wp_json_encode(array('pending_activation' => TRUE, 'subscription_id' => $subscription_id, 'status' => (string)$fulfillment['status'])); | |
| 782 | + exit(); | |
| 783 | + } | |
| 784 | + if(empty($fulfillment['ok']) || empty($fulfillment['rtn_url']) || empty($fulfillment['rtn_post'])) | |
| 785 | + { | |
| 786 | + echo wp_json_encode(array('error' => !empty($fulfillment['error']) ? (string)$fulfillment['error'] : 'subscription_fulfillment_failed')); | |
| 787 | + exit(); | |
| 788 | + } | |
| 789 | + echo wp_json_encode(array('rtn_url' => $fulfillment['rtn_url'], 'rtn_post' => $fulfillment['rtn_post'])); | |
| 790 | + exit(); | |
| 791 | + } | |
| 792 | + | |
| 686 | 793 | if($gateway_checkout_id) |
| 687 | 794 | { |
| 688 | 795 | if(in_array($status, array('APPROVAL_PENDING', 'APPROVED'), TRUE)) |
| 689 | 796 | { |
| @@ -709,9 +816,9 @@ | ||
| 709 | 816 | } |
| 710 | 817 | else if(!in_array($status, array('ACTIVE', 'APPROVED', 'APPROVAL_PENDING'), TRUE) && !$allow_expired_single_cycle) |
| 711 | 818 | { |
| 712 | 819 | //260902.0200 Preserve existing non-coordinator PayPal Checkout button behavior until those flows migrate onto Gateway Checkout and gain the same activation polling. |
| 713 | - //260907.2142 TO-DO: Migrate maintained Framework PayPal Checkout button/redirect flows onto Gateway Checkout before claiming cross-surface PPCO dedupe/idempotency parity, preserving the Pro-Form guarantees for durable provider identity, stable idempotent retries, monotonic final-state recovery, and shared browser/webhook fulfillment dedupe. | |
| 820 | + //260928.1645 Existing pre-migration browser tabs still carry the legacy PayPal Checkout token without Gateway Checkout identity. Preserve their original confirmation behavior until those in-flight tokens expire. | |
| 714 | 821 | c_ws_plugin__s2member_utils_logs::log_entry('paypal-checkout', array( |
| 715 | 822 | 'ppco' => 'checkout', |
| 716 | 823 | 'env_setting' => $env_setting, |
| 717 | 824 | 'event' => 'subscription_status_invalid', |
| @@ -846,9 +953,9 @@ | ||
| 846 | 953 | c_ws_plugin__s2member_utils_logs::log_entry('paypal-checkout', array( |
| 847 | 954 | 'ppco' => 'checkout', |
| 848 | 955 | 'env_setting' => $env_setting, |
| 849 | 956 | 'event' => 'cancel_subscription_not_logged_in', |
| 850 | - 'token' => $token, | |
| 957 | + 'token' => $log_token, | |
| 851 | 958 | )); |
| 852 | 959 | |
| 853 | 960 | echo wp_json_encode(array('error' => 'not_logged_in')); |
| 854 | 961 | exit(); |
| @@ -878,9 +985,9 @@ | ||
| 878 | 985 | 'ppco' => 'checkout', |
| 879 | 986 | 'env_setting' => $env_setting, |
| 880 | 987 | 'event' => 'cancel_subscription_token_mismatch', |
| 881 | 988 | 'user_id' => $user_id, |
| 882 | - 'token' => $token, | |
| 989 | + 'token' => $log_token, | |
| 883 | 990 | )); |
| 884 | 991 | |
| 885 | 992 | echo wp_json_encode(array('error' => 'token_mismatch')); |
| 886 | 993 | exit(); |
| @@ -1019,9 +1126,9 @@ | ||
| 1019 | 1126 | 'ppco' => 'checkout', |
| 1020 | 1127 | 'env_setting' => $env_setting, |
| 1021 | 1128 | 'event' => 'create_order_response', |
| 1022 | 1129 | 'order' => $order, |
| 1023 | - 'token' => $token, | |
| 1130 | + 'token' => $log_token, | |
| 1024 | 1131 | )); |
| 1025 | 1132 | |
| 1026 | 1133 | if(empty($order['id'])) |
| 1027 | 1134 | { |
| @@ -1029,9 +1136,9 @@ | ||
| 1029 | 1136 | 'ppco' => 'checkout', |
| 1030 | 1137 | 'env_setting' => $env_setting, |
| 1031 | 1138 | 'event' => 'order_create_failed', |
| 1032 | 1139 | 'order' => $order, |
| 1033 | - 'token' => $token, | |
| 1140 | + 'token' => $log_token, | |
| 1034 | 1141 | )); |
| 1035 | 1142 | |
| 1036 | 1143 | $error = !empty($order['__error']) ? (string)$order['__error'] : 'order_create_failed'; |
| 1037 | 1144 | $recoverable = ($error === 'gateway_checkout_busy'); |
| @@ -1045,9 +1152,10 @@ | ||
| 1045 | 1152 | else if($op === 'get_order_status') |
| 1046 | 1153 | { |
| 1047 | 1154 | //260907.1820 This recovery endpoint is intentionally coordinator-only: the signed checkout token authorizes a local state read, while PayPal polling/retries remain server/webhook responsibilities. |
| 1048 | 1155 | $gateway_checkout_id = !empty($token['gateway_checkout_id']) && c_ws_plugin__s2member_gateway_checkouts::valid_id((string)$token['gateway_checkout_id']) ? (string)$token['gateway_checkout_id'] : ''; |
| 1049 | - $gateway_checkout = $gateway_checkout_id ? c_ws_plugin__s2member_gateway_checkouts::get($gateway_checkout_id) : FALSE; | |
| 1156 | + //260928.1703 Read fresh option state during capture-loss polling: a webhook may have fulfilled the checkout in another PHP worker moments earlier. | |
| 1157 | + $gateway_checkout = $gateway_checkout_id ? c_ws_plugin__s2member_gateway_checkouts::load_state_uncached($gateway_checkout_id) : FALSE; | |
| 1050 | 1158 | if(!$gateway_checkout || (string)$gateway_checkout['gateway'] !== 'paypal_checkout' || (string)$gateway_checkout['operation'] !== 'payment') |
| 1051 | 1159 | { |
| 1052 | 1160 | echo wp_json_encode(array('error' => 'gateway_checkout_invalid')); |
| 1053 | 1161 | exit(); |
| @@ -1055,15 +1163,23 @@ | ||
| 1055 | 1163 | |
| 1056 | 1164 | $private_context = c_ws_plugin__s2member_gateway_checkouts::private_context_get($gateway_checkout_id); |
| 1057 | 1165 | $fulfillment_result = is_array($private_context) && !empty($private_context['paypal_checkout']['fulfillment_result']) && is_array($private_context['paypal_checkout']['fulfillment_result']) ? $private_context['paypal_checkout']['fulfillment_result'] : array(); |
| 1058 | 1166 | //260902.0635 Poll only local coordinator state while independent PayPal webhooks resolve delayed creates/captures; do not hammer the provider from the browser. |
| 1059 | - echo wp_json_encode(array( | |
| 1167 | + $fulfilled = ((string)$gateway_checkout['fulfillment_status'] === 'fulfilled' && !empty($fulfillment_result['rtn_url']) && !empty($fulfillment_result['rtn_post'])); | |
| 1168 | + $response = array( | |
| 1060 | 1169 | 'order_id' => !empty($gateway_checkout['gateway_ids']['order_id']) ? (string)$gateway_checkout['gateway_ids']['order_id'] : '', |
| 1061 | 1170 | 'capture_id' => !empty($gateway_checkout['gateway_ids']['capture_id']) ? (string)$gateway_checkout['gateway_ids']['capture_id'] : '', |
| 1062 | 1171 | 'status' => !empty($gateway_checkout['gateway_status']) ? (string)$gateway_checkout['gateway_status'] : '', |
| 1063 | 1172 | 'fulfillment_status' => !empty($gateway_checkout['fulfillment_status']) ? (string)$gateway_checkout['fulfillment_status'] : '', |
| 1064 | - 'fulfilled' => ((string)$gateway_checkout['fulfillment_status'] === 'fulfilled' && !empty($fulfillment_result)), | |
| 1065 | - )); | |
| 1173 | + 'fulfilled' => $fulfilled, | |
| 1174 | + ); | |
| 1175 | + //260928.1703 A lost capture response can be recovered with the already-signed return handoff; only the original encrypted checkout token can reach this endpoint. | |
| 1176 | + if($fulfilled) | |
| 1177 | + { | |
| 1178 | + $response['rtn_url'] = $fulfillment_result['rtn_url']; | |
| 1179 | + $response['rtn_post'] = $fulfillment_result['rtn_post']; | |
| 1180 | + } | |
| 1181 | + echo wp_json_encode($response); | |
| 1066 | 1182 | exit(); |
| 1067 | 1183 | } |
| 1068 | 1184 | else if($op === 'capture_order') |
| 1069 | 1185 | { |
| @@ -1077,9 +1193,9 @@ | ||
| 1077 | 1193 | |
| 1078 | 1194 | $gateway_checkout_id = !empty($token['gateway_checkout_id']) && c_ws_plugin__s2member_gateway_checkouts::valid_id((string)$token['gateway_checkout_id']) ? (string)$token['gateway_checkout_id'] : ''; |
| 1079 | 1195 | if($gateway_checkout_id) |
| 1080 | 1196 | { |
| 1081 | - $gateway_checkout = c_ws_plugin__s2member_gateway_checkouts::get($gateway_checkout_id); | |
| 1197 | + $gateway_checkout = c_ws_plugin__s2member_gateway_checkouts::load_state($gateway_checkout_id); | |
| 1082 | 1198 | $private_context = $gateway_checkout ? c_ws_plugin__s2member_gateway_checkouts::private_context_get($gateway_checkout_id) : FALSE; |
| 1083 | 1199 | $fulfillment_result = is_array($private_context) && !empty($private_context['paypal_checkout']['fulfillment_result']) && is_array($private_context['paypal_checkout']['fulfillment_result']) ? $private_context['paypal_checkout']['fulfillment_result'] : array(); |
| 1084 | 1200 | if($gateway_checkout && (string)$gateway_checkout['fulfillment_status'] === 'fulfilled' && !empty($fulfillment_result['rtn_url']) && !empty($fulfillment_result['rtn_post'])) |
| 1085 | 1201 | { |
| @@ -1102,9 +1218,9 @@ | ||
| 1102 | 1218 | 'amount' => !empty($cap0['amount']['value']) ? (string)$cap0['amount']['value'] : '', |
| 1103 | 1219 | 'cc' => !empty($cap0['amount']['currency_code']) ? (string)$cap0['amount']['currency_code'] : '', |
| 1104 | 1220 | 'payer' => !empty($capture['payer']['email_address']) ? (string)$capture['payer']['email_address'] : '', |
| 1105 | 1221 | 'capture' => $capture, |
| 1106 | - 'token' => $token, | |
| 1222 | + 'token' => $log_token, | |
| 1107 | 1223 | )); |
| 1108 | 1224 | |
| 1109 | 1225 | if($gateway_checkout_id) |
| 1110 | 1226 | { |
| @@ -1157,9 +1273,9 @@ | ||
| 1157 | 1273 | 'env_setting' => $env_setting, |
| 1158 | 1274 | 'event' => 'capture_missing_fields', |
| 1159 | 1275 | 'order_id' => $order_id, |
| 1160 | 1276 | 'capture' => $capture, |
| 1161 | - 'token' => $token, | |
| 1277 | + 'token' => $log_token, | |
| 1162 | 1278 | )); |
| 1163 | 1279 | |
| 1164 | 1280 | echo wp_json_encode(array('error' => 'capture_missing_fields')); |
| 1165 | 1281 | exit(); |
| @@ -1173,9 +1289,9 @@ | ||
| 1173 | 1289 | 'ppco' => 'checkout', |
| 1174 | 1290 | 'env_setting' => $env_setting, |
| 1175 | 1291 | 'event' => 'amount_mismatch', |
| 1176 | 1292 | 'order_id' => $order_id, |
| 1177 | - 'token' => $token, | |
| 1293 | + 'token' => $log_token, | |
| 1178 | 1294 | 'pu' => array('amount' => $pu_amount, 'cc' => $pu_cc), |
| 1179 | 1295 | )); |
| 1180 | 1296 | echo wp_json_encode(array('error' => 'amount_mismatch')); |
| 1181 | 1297 | exit(); |
| @@ -1186,9 +1302,9 @@ | ||
| 1186 | 1302 | 'ppco' => 'checkout', |
| 1187 | 1303 | 'env_setting' => $env_setting, |
| 1188 | 1304 | 'event' => 'currency_mismatch', |
| 1189 | 1305 | 'order_id' => $order_id, |
| 1190 | - 'token' => $token, | |
| 1306 | + 'token' => $log_token, | |
| 1191 | 1307 | 'pu' => array('amount' => $pu_amount, 'cc' => $pu_cc), |
| 1192 | 1308 | )); |
| 1193 | 1309 | echo wp_json_encode(array('error' => 'currency_mismatch')); |
| 1194 | 1310 | exit(); |
| @@ -1205,9 +1321,9 @@ | ||
| 1205 | 1321 | 'ppco' => 'checkout', |
| 1206 | 1322 | 'env_setting' => $env_setting, |
| 1207 | 1323 | 'event' => 'invoice_mismatch', |
| 1208 | 1324 | 'order_id' => $order_id, |
| 1209 | - 'token' => $token, | |
| 1325 | + 'token' => $log_token, | |
| 1210 | 1326 | 'invoice' => $cap_invoice_id, |
| 1211 | 1327 | )); |
| 1212 | 1328 | echo wp_json_encode(array('error' => 'invoice_mismatch')); |
| 1213 | 1329 | exit(); |
| @@ -1225,9 +1341,9 @@ | ||
| 1225 | 1341 | 'ppco' => 'checkout', |
| 1226 | 1342 | 'env_setting' => $env_setting, |
| 1227 | 1343 | 'event' => 'custom_mismatch', |
| 1228 | 1344 | 'order_id' => $order_id, |
| 1229 | - 'token' => $token, | |
| 1345 | + 'token' => $log_token, | |
| 1230 | 1346 | 'custom' => array( |
| 1231 | 1347 | 'token' => !empty($token['custom']) ? $token['custom'] : '', |
| 1232 | 1348 | 'paypal' => $cap_custom_id, |
| 1233 | 1349 | ), |