'checkout', 'env_setting' => $env_setting, 'event' => 'request', 'get' => $_GET, 'post' => $_POST, 'method' => !empty($_SERVER['REQUEST_METHOD']) ? $_SERVER['REQUEST_METHOD'] : '', 'ip' => !empty($_SERVER['REMOTE_ADDR']) ? $_SERVER['REMOTE_ADDR'] : '', 'ua' => !empty($_SERVER['HTTP_USER_AGENT']) ? $_SERVER['HTTP_USER_AGENT'] : '', 'referer' => !empty($_SERVER['HTTP_REFERER']) ? $_SERVER['HTTP_REFERER'] : '', )); if(!$op || !$t) { echo wp_json_encode(array('error' => 'missing_op_or_token')); exit(); } $raw = c_ws_plugin__s2member_utils_encryption::decrypt($t); //260808 Safely unserialize the PayPal checkout token. $token = c_ws_plugin__s2member_utils_arrays::maybe_unserialize($raw); if(!is_array($token)) $token = false; if(!$token || !is_array($token)) { echo wp_json_encode(array('error' => 'invalid_token')); exit(); } if(!empty($token['exp']) && is_numeric($token['exp']) && time() > (int)$token['exp']) { echo wp_json_encode(array('error' => 'token_expired')); exit(); } if(empty($token['invoice']) || empty($token['ip']) || empty($token['item_number']) || empty($token['checksum'])) { echo wp_json_encode(array('error' => 'token_incomplete')); exit(); } if($token['checksum'] !== md5($token['invoice'].$token['ip'].$token['item_number'])) { echo wp_json_encode(array('error' => 'token_checksum_mismatch')); exit(); } if($token['ip'] !== c_ws_plugin__s2member_utils_ip::current()) { //260414 PayPal Checkout browser returns can legitimately arrive with a different client IP; log it, but do not fail the token. c_ws_plugin__s2member_utils_logs::log_entry('paypal-checkout', array( 'ppco' => 'checkout', 'env_setting' => $env_setting, 'event' => 'token_ip_mismatch', 'token' => $token, 'ip' => c_ws_plugin__s2member_utils_ip::current(), )); } $old__subscr_gateway = !empty($token['old__subscr_gateway']) ? (string)$token['old__subscr_gateway'] : ''; $old__subscr_id = !empty($token['old__subscr_id']) ? (string)$token['old__subscr_id'] : ''; $old__subscr_baid = !empty($token['old__subscr_baid']) ? (string)$token['old__subscr_baid'] : ''; $old__subscr_cid = !empty($token['old__subscr_cid']) ? (string)$token['old__subscr_cid'] : ''; $old__ipn_signup_vars = (!empty($token['old__ipn_signup_vars']) && is_array($token['old__ipn_signup_vars'])) ? $token['old__ipn_signup_vars'] : array(); //260408 Use the old context captured before the buyer left for PayPal. // output="anchor|url" support: redirect-mode endpoints (GET). if($op === 'redirect' || $op === 'return' || $op === 'cancel') { // NOTE: These endpoints are intended for output="anchor|url" shortcode formats. // They redirect to PayPal approval URLs, then auto-POST into s2Member's existing PayPal notify + return handlers. if($op === 'cancel') { $cancel = !empty($token['cancel']) ? (string)$token['cancel'] : home_url('/'); $cancel = wp_validate_redirect($cancel, home_url('/')); wp_redirect($cancel); exit(); } $endpoint = home_url('/?s2member_paypal_checkout=1'); $return_url = $endpoint.'&s2member_paypal_checkout_op=return&s2member_paypal_checkout_t='.rawurlencode($t); $cancel_url = $endpoint.'&s2member_paypal_checkout_op=cancel&s2member_paypal_checkout_t='.rawurlencode($t); if($op === 'redirect') { $pp_token = $token; $pp_token['return'] = $return_url; $pp_token['cancel'] = $cancel_url; if((!isset($pp_token['rr']) || (string)$pp_token['rr'] === '') || strtoupper((string)$pp_token['rr']) === 'BN') { $order = c_ws_plugin__s2member_paypal_utilities::paypal_checkout_order_create($pp_token); c_ws_plugin__s2member_utils_logs::log_entry('paypal-checkout', array( 'ppco' => 'checkout', 'env_setting' => $env_setting, 'event' => 'redirect_order_create_response', 'order' => $order, 'token' => $token, )); $approve_url = ''; if(!empty($order['links']) && is_array($order['links'])) foreach($order['links'] as $link) if(!empty($link['rel']) && !empty($link['href'])) { $rel = strtolower((string)$link['rel']); if($rel === 'approve' || $rel === 'payer-action' || $rel === 'approval_url') $approve_url = (string)$link['href']; } if(!$approve_url) { echo 'order_approval_url_missing'; exit(); } wp_redirect($approve_url); exit(); } else { $subscription = c_ws_plugin__s2member_paypal_utilities::paypal_checkout_subscription_create($pp_token); c_ws_plugin__s2member_utils_logs::log_entry('paypal-checkout', array( 'ppco' => 'checkout', 'env_setting' => $env_setting, 'event' => 'redirect_subscription_create_response', 'subscription' => $subscription, 'token' => $token, )); $approve_url = ''; if(!empty($subscription['links']) && is_array($subscription['links'])) foreach($subscription['links'] as $link) if(!empty($link['rel']) && !empty($link['href']) && strtolower((string)$link['rel']) === 'approve') $approve_url = (string)$link['href']; if(!$approve_url) { echo 'subscription_approval_url_missing'; exit(); } wp_redirect($approve_url); exit(); } } // Return URL: PayPal redirects here after approval. if((!isset($token['rr']) || (string)$token['rr'] === '') || strtoupper((string)$token['rr']) === 'BN') { $order_id = !empty($_GET['token']) ? trim(stripslashes((string)$_GET['token'])) : ''; if(!$order_id) { echo 'missing_order_id'; exit(); } $capture = c_ws_plugin__s2member_paypal_utilities::paypal_checkout_order_capture($order_id, $token); $cap0 = (!empty($capture['purchase_units'][0]['payments']['captures'][0]) && is_array($capture['purchase_units'][0]['payments']['captures'][0])) ? $capture['purchase_units'][0]['payments']['captures'][0] : array(); c_ws_plugin__s2member_utils_logs::log_entry('paypal-checkout', array( 'ppco' => 'checkout', 'env_setting' => $env_setting, 'event' => 'capture_response', 'order_id' => $order_id, 'status' => !empty($capture['status']) ? (string)$capture['status'] : '', 'capture_id' => !empty($cap0['id']) ? (string)$cap0['id'] : '', 'amount' => !empty($cap0['amount']['value']) ? (string)$cap0['amount']['value'] : '', 'cc' => !empty($cap0['amount']['currency_code']) ? (string)$cap0['amount']['currency_code'] : '', 'payer' => !empty($capture['payer']['email_address']) ? (string)$capture['payer']['email_address'] : '', 'capture' => $capture, 'token' => $token, )); if(!empty($capture['__error'])) { echo (string)$capture['__error']; exit(); } if(empty($capture['status']) || strtoupper($capture['status']) !== 'COMPLETED') { echo 'order_capture_failed'; exit(); } //260818.0126 Keep submitted Pro-Form contact details for pro-emails; they may differ from the payer's PayPal profile. $is_pro_form = (!empty($token['s2member_paypal_proxy_use']) && (string)$token['s2member_paypal_proxy_use'] === 'pro-emails'); $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'] : ''); $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'] : ''); $last_name = ($is_pro_form && isset($token['last_name'])) ? (string)$token['last_name'] : (!empty($capture['payer']['name']['surname']) ? (string)$capture['payer']['name']['surname'] : ''); $pu_amount = !empty($capture['purchase_units'][0]['payments']['captures'][0]['amount']['value']) ? (string)$capture['purchase_units'][0]['payments']['captures'][0]['amount']['value'] : ''; $pu_cc = !empty($capture['purchase_units'][0]['payments']['captures'][0]['amount']['currency_code']) ? (string)$capture['purchase_units'][0]['payments']['captures'][0]['amount']['currency_code'] : ''; $pu_cap_id = !empty($capture['purchase_units'][0]['payments']['captures'][0]['id']) ? (string)$capture['purchase_units'][0]['payments']['captures'][0]['id'] : ''; if(!$payer_email || !$pu_amount || !$pu_cc || !$pu_cap_id) { echo 'capture_missing_fields'; exit(); } //260228 Normalize amount strings before comparison (e.g. 20 vs 20.00). if(!empty($token['amount']) && number_format((float)$token['amount'], 2, '.', '') !== number_format((float)$pu_amount, 2, '.', '')) { echo 'amount_mismatch'; exit(); } if(!empty($token['cc']) && strtoupper((string)$token['cc']) !== strtoupper((string)$pu_cc)) { echo 'currency_mismatch'; exit(); } $paypal = array( 'txn_type' => 'web_accept', 'payment_status' => 'Completed', 'txn_id' => $pu_cap_id, 'mc_gross' => $pu_amount, 'mc_currency' => $pu_cc, 'invoice' => (string)$token['invoice'], 'custom' => (string)$token['custom'], 'item_name' => (string)$token['item_name'], 'item_number' => (string)$token['item_number'], 'option_name1' => (string)$token['on0'], 'option_selection1' => (string)$token['os0'], 'option_name2' => (string)$token['on1'], 'option_selection2' => (string)$token['os1'], 'payer_email' => $payer_email, 'first_name' => $first_name, 'last_name' => $last_name, ); //260817.2119 Preserve Pro-Form tax in the simulated IPN so existing fulfillment and email logic receives the same calculated values as the legacy Pro flow. if(isset($token['tax'])) $paypal['tax'] = (string)$token['tax']; $is_independent_ccaps_sale = (strpos((string)$token['item_number'], '*:') === 0); $is_specific_post_page_sale = (strpos((string)$token['item_number'], 'sp:') === 0); $can_cancel_old_subscr = (!$is_independent_ccaps_sale && !$is_specific_post_page_sale); //260407 Only membership replacement-style PPCO purchases should cancel an existing recurring subscription here. //260817.2119 Keep normal Checkout defaults while allowing an encrypted Pro-Form token to request its existing email, coupon, and success-URL handling during the internal Notify call. $proxy_use = !empty($token['s2member_paypal_proxy_use']) ? (string)$token['s2member_paypal_proxy_use'] : 'paypal_checkout'; $notify_extra = array(); if(!empty($token['s2member_paypal_proxy_coupon']) && is_array($token['s2member_paypal_proxy_coupon'])) $notify_extra['s2member_paypal_proxy_coupon'] = $token['s2member_paypal_proxy_coupon']; if(array_key_exists('s2member_paypal_proxy_return_url', $token)) $notify_extra['s2member_paypal_proxy_return_url'] = (string)$token['s2member_paypal_proxy_return_url']; $notify_done_option = 's2m_ppco_capture_done_'.md5($pu_cap_id); $notify_result = c_ws_plugin__s2member_paypal_utilities::paypal_checkout_notify_once($paypal, $notify_done_option, $proxy_use, $notify_extra); if(empty($notify_result['ok'])) { if($is_redirect_mode) echo !empty($notify_result['error']) ? (string)$notify_result['error'] : 'notify_proxy_failed'; else { if(!headers_sent()) status_header(500); echo wp_json_encode(array('error' => !empty($notify_result['error']) ? (string)$notify_result['error'] : 'notify_proxy_failed')); } exit(); } //260817 Only the request that actually performed fulfillment should trigger replacement-subscription cancellation. if(!empty($notify_result['processed']) && $can_cancel_old_subscr && $old__subscr_id && apply_filters('s2member_pro_cancels_old_rp_before_new_rp', ($old__subscr_id !== $pu_cap_id), get_defined_vars())) //260406 c_ws_plugin__s2member_utilities::cancel_gateway_subscription($old__subscr_gateway, $old__subscr_id, $old__subscr_baid, $old__subscr_cid, $old__ipn_signup_vars); //260407 $return_url = (string)$token['return']; $return_url = add_query_arg('s2member_paypal_proxy', 'paypal', $return_url); $return_post = array_merge($paypal, array( 's2member_paypal_proxy' => 'paypal', 's2member_paypal_proxy_use' => $proxy_use, )); //260817 Carry the already-resolved Pro-Form success URL inside the signed browser-return package. if(array_key_exists('s2member_paypal_proxy_return_url', $token)) $return_post['s2member_paypal_proxy_return_url'] = !empty($notify_result['body']) ? trim((string)$notify_result['body']) : ''; //260817 Sign the exact browser-return payload without exposing the reusable internal PayPal proxy key. $return_handoff = c_ws_plugin__s2member_paypal_utilities::paypal_checkout_return_handoff_create($return_post); if(!$return_handoff) { echo 'return_handoff_failed'; exit(); } $return_post['s2member_paypal_checkout_handoff'] = $return_handoff; // Auto-POST into s2Member's existing PayPal return handler. echo ''; echo '
'; //260817 Keep the signed browser-return payload encoding stable. foreach($return_post as $k => $v) echo ''; echo '
'; exit(); } else { $subscription_id = !empty($_GET['subscription_id']) ? trim(stripslashes((string)$_GET['subscription_id'])) : ''; if(!$subscription_id) { echo 'missing_subscription_id'; exit(); } $subscription_r = c_ws_plugin__s2member_paypal_utilities::paypal_checkout_api_request('GET', '/v1/billing/subscriptions/'.rawurlencode($subscription_id)); c_ws_plugin__s2member_utils_logs::log_entry('paypal-checkout', array( 'ppco' => 'checkout', 'env_setting' => $env_setting, 'event' => 'subscription_get_response', 'subscription_id' => $subscription_id, 'code' => !empty($subscription_r['code']) ? (int)$subscription_r['code'] : 0, 'body' => !empty($subscription_r['body']) ? (string)$subscription_r['body'] : '', 'token' => $token, )); $subscription_code = !empty($subscription_r['code']) ? (int)$subscription_r['code'] : 0; $subscription_body = !empty($subscription_r['body']) ? (string)$subscription_r['body'] : ''; $subscription = array(); if($subscription_body) $subscription = json_decode($subscription_body, true); if(!is_array($subscription)) $subscription = array(); if($subscription_code < 200 || $subscription_code > 299 || empty($subscription['id'])) { echo 'subscription_get_failed'; exit(); } $custom_id = !empty($subscription['custom_id']) ? (string)$subscription['custom_id'] : ''; if($custom_id && (string)$token['invoice'] && $custom_id !== (string)$token['invoice']) { echo 'subscription_custom_id_mismatch'; exit(); } $subscriber_email = !empty($subscription['subscriber']['email_address']) ? (string)$subscription['subscriber']['email_address'] : ''; $first_name = !empty($subscription['subscriber']['name']['given_name']) ? (string)$subscription['subscriber']['name']['given_name'] : ''; $last_name = !empty($subscription['subscriber']['name']['surname']) ? (string)$subscription['subscriber']['name']['surname'] : ''; $paypal = array( 'txn_type' => 'subscr_signup', 'payment_status' => 'Completed', 'subscr_gateway' => 'paypal', 'txn_id' => $subscription_id, 'subscr_id' => $subscription_id, 'subscr_baid' => $subscription_id, 'subscr_cid' => $subscription_id, 'mc_gross' => (string)$token['amount'], 'mc_currency' => strtoupper((string)$token['cc']), 'period1' => (!empty($token['tp']) && !empty($token['tt'])) ? ((string)$token['tp'].' '.strtoupper((string)$token['tt'])) : '0 D', 'mc_amount1' => (!empty($token['tp']) && !empty($token['tt'])) ? (string)$token['ta'] : '0.00', 'period3' => ((string)$token['rp'].' '.strtoupper((string)$token['rt'])), 'mc_amount3' => (string)$token['amount'], 'recurring' => ((isset($token['rr']) && (string)$token['rr'] === '1') ? '1' : '0'), 'invoice' => (string)$token['invoice'], 'custom' => (string)$token['custom'], 'item_name' => (string)$token['item_name'], 'item_number' => (string)$token['item_number'], 'payer_email' => $subscriber_email, 'first_name' => $first_name, 'last_name' => $last_name, 'option_name1' => (string)$token['on0'], 'option_selection1' => (string)$token['os0'], 'option_name2' => (string)$token['on1'], 'option_selection2' => (string)$token['os1'], ); $option_ppco_subscr = 's2m_ppco_subscr_done_'.md5($subscription_id); //260818.0603 Share the success-only Notify lock/done marker with browser confirmation and webhook activation fallback. $notify_result = c_ws_plugin__s2member_paypal_utilities::paypal_checkout_notify_once($paypal, $option_ppco_subscr); if(empty($notify_result['ok'])) { echo !empty($notify_result['error']) ? (string)$notify_result['error'] : 'notify_proxy_failed'; exit(); } //260818.0603 Only the request that completed Notify should cancel a replaced subscription; duplicates are already fulfilled. if(!empty($notify_result['processed']) && $old__subscr_id && apply_filters('s2member_pro_cancels_old_rp_before_new_rp', ($old__subscr_id !== $subscription_id), get_defined_vars())) c_ws_plugin__s2member_utilities::cancel_gateway_subscription($old__subscr_gateway, $old__subscr_id, $old__subscr_baid, $old__subscr_cid, $old__ipn_signup_vars); $return_url2 = (string)$token['return']; $return_url2 = add_query_arg('s2member_paypal_proxy', 'paypal', $return_url2); $return_post2 = array_merge($paypal, array( 's2member_paypal_proxy' => 'paypal', 's2member_paypal_proxy_use' => 'paypal_checkout', )); //260817 Sign the exact browser-return payload without exposing the reusable internal PayPal proxy key. $return_handoff = c_ws_plugin__s2member_paypal_utilities::paypal_checkout_return_handoff_create($return_post2); if(!$return_handoff) { echo 'return_handoff_failed'; exit(); } $return_post2['s2member_paypal_checkout_handoff'] = $return_handoff; echo ''; echo '
'; //260817 Keep the signed browser-return payload encoding stable. foreach($return_post2 as $k => $v) echo ''; echo '
'; exit(); } } if($op === 'create_subscription') { if((!isset($token['rr']) || (string)$token['rr'] === '') || strtoupper((string)$token['rr']) === 'BN') { echo wp_json_encode(array('error' => 'not_subscription')); exit(); } $subscription = c_ws_plugin__s2member_paypal_utilities::paypal_checkout_subscription_create($token); c_ws_plugin__s2member_utils_logs::log_entry('paypal-checkout', array( 'ppco' => 'checkout', 'env_setting' => $env_setting, 'event' => 'create_subscription_response', 'subscription' => $subscription, 'token' => $token, )); if(empty($subscription['id'])) { $error = !empty($subscription['__error']) ? (string)$subscription['__error'] : 'subscription_create_failed'; $recoverable = in_array($error, array('subscription_create_unresolved', 'gateway_checkout_busy'), TRUE); //260902.0200 Let coordinator-backed browser flows briefly wait for webhook repair only when creation is genuinely unresolved/in progress; deterministic failures remain immediate errors. echo wp_json_encode(array('error' => $error, 'recoverable' => $recoverable)); exit(); } //260901.2145 The browser receives only the already-persisted PayPal subscription ID; PayPal's JS SDK handles buyer approval from that server-created resource. echo wp_json_encode(array('subscription_id' => (string)$subscription['id'])); exit(); } if($op === 'get_subscription_id') { if((!isset($token['rr']) || (string)$token['rr'] === '') || strtoupper((string)$token['rr']) === 'BN') { echo wp_json_encode(array('error' => 'not_subscription')); exit(); } $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'] : ''; $gateway_checkout = $gateway_checkout_id ? c_ws_plugin__s2member_gateway_checkouts::load_state($gateway_checkout_id) : FALSE; if(!$gateway_checkout || (string)$gateway_checkout['gateway'] !== 'paypal_checkout' || (string)$gateway_checkout['operation'] !== 'subscription') { echo wp_json_encode(array('error' => 'gateway_checkout_invalid')); exit(); } $subscription_id = !empty($gateway_checkout['gateway_ids']['subscription_id']) ? (string)$gateway_checkout['gateway_ids']['subscription_id'] : ''; //260902.0200 This poll reads only local coordinator state; PayPal is not called repeatedly while a CREATED webhook has a chance to repair an ambiguous create response. echo wp_json_encode(array( 'subscription_id' => $subscription_id, 'pending' => !$subscription_id, 'status' => !empty($gateway_checkout['gateway_status']) ? (string)$gateway_checkout['gateway_status'] : '', )); exit(); } if($op === 'get_plan_id') { if((!isset($token['rr']) || (string)$token['rr'] === '') || strtoupper((string)$token['rr']) === 'BN') { echo wp_json_encode(array('error' => 'not_subscription')); exit(); } $plan_id = c_ws_plugin__s2member_paypal_utilities::paypal_checkout_plan_get_id($token); c_ws_plugin__s2member_utils_logs::log_entry('paypal-checkout', array( 'ppco' => 'checkout', 'env_setting' => $env_setting, 'event' => 'get_plan_id_response', 'plan_id' => $plan_id, 'token' => $token, )); if(!$plan_id) { echo wp_json_encode(array('error' => 'plan_create_failed')); exit(); } echo wp_json_encode(array('plan_id' => $plan_id)); exit(); } if($op === 'confirm_subscription') { if((!isset($token['rr']) || (string)$token['rr'] === '') || strtoupper((string)$token['rr']) === 'BN') { echo wp_json_encode(array('error' => 'not_subscription')); exit(); } $subscription_id = !empty($_POST['subscription_id']) ? trim(stripslashes((string)$_POST['subscription_id'])) : ''; if(!$subscription_id) { echo wp_json_encode(array('error' => 'missing_subscription_id')); exit(); } $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'] : ''; if($gateway_checkout_id) { $gateway_checkout = c_ws_plugin__s2member_gateway_checkouts::load_state($gateway_checkout_id); $expected_subscription_id = $gateway_checkout && !empty($gateway_checkout['gateway_ids']['subscription_id']) ? (string)$gateway_checkout['gateway_ids']['subscription_id'] : ''; //260901.2145 A coordinator-backed browser may confirm only the PayPal subscription that s2Member created and persisted for this logical checkout. if(!$expected_subscription_id || !hash_equals($expected_subscription_id, $subscription_id)) { echo wp_json_encode(array('error' => 'gateway_checkout_subscription_mismatch')); exit(); } } $subscription_r = c_ws_plugin__s2member_paypal_utilities::paypal_checkout_api_request('GET', '/v1/billing/subscriptions/'.rawurlencode($subscription_id)); c_ws_plugin__s2member_utils_logs::log_entry('paypal-checkout', array( 'ppco' => 'checkout', 'env_setting' => $env_setting, 'event' => 'subscription_get_response', 'subscription_id' => $subscription_id, 'subscription' => $subscription_r, 'token' => $token, )); $subscription_code = !empty($subscription_r['code']) ? (int)$subscription_r['code'] : 0; $subscription_body = !empty($subscription_r['body']) ? (string)$subscription_r['body'] : ''; $subscription = array(); if($subscription_body) $subscription = json_decode($subscription_body, true); if(!is_array($subscription)) $subscription = array(); if($subscription_code < 200 || $subscription_code > 299 || empty($subscription['id'])) { c_ws_plugin__s2member_utils_logs::log_entry('paypal-checkout', array( 'ppco' => 'checkout', 'env_setting' => $env_setting, 'event' => 'subscription_get_failed', 'subscription_id' => $subscription_id, 'code' => $subscription_code, 'body' => $subscription_body, )); echo wp_json_encode(array('error' => 'subscription_get_failed')); exit(); } $status = !empty($subscription['status']) ? strtoupper((string)$subscription['status']) : ''; $is_single_cycle = (isset($token['rr']) && (string)$token['rr'] === '0'); $allow_expired_single_cycle = false; // PayPal can complete a single-cycle subscription immediately, returning status=EXPIRED after payment. if($is_single_cycle && $status === 'EXPIRED') { $lpv = ''; $lpc = ''; if(!empty($subscription['billing_info']['last_payment']['amount']['value'])) $lpv = (string)$subscription['billing_info']['last_payment']['amount']['value']; if(!empty($subscription['billing_info']['last_payment']['amount']['currency_code'])) $lpc = strtoupper((string)$subscription['billing_info']['last_payment']['amount']['currency_code']); if($lpv !== '' && $lpc !== '') $allow_expired_single_cycle = true; } if(!$status) { c_ws_plugin__s2member_utils_logs::log_entry('paypal-checkout', array( 'ppco' => 'checkout', 'env_setting' => $env_setting, 'event' => 'subscription_status_invalid', 'subscription_id' => $subscription_id, 'status' => $status, )); echo wp_json_encode(array('error' => 'subscription_status_invalid')); exit(); } $expected_plan_id = c_ws_plugin__s2member_paypal_utilities::paypal_checkout_plan_get_id($token); if($expected_plan_id && !empty($subscription['plan_id']) && (string)$subscription['plan_id'] !== (string)$expected_plan_id) { c_ws_plugin__s2member_utils_logs::log_entry('paypal-checkout', array( 'ppco' => 'checkout', 'env_setting' => $env_setting, 'event' => 'plan_mismatch', 'subscription_id' => $subscription_id, 'expected' => $expected_plan_id, 'actual' => (string)$subscription['plan_id'], )); echo wp_json_encode(array('error' => 'plan_mismatch')); exit(); } $custom_id = !empty($subscription['custom_id']) ? (string)$subscription['custom_id'] : ''; if($custom_id && $custom_id !== (string)$token['invoice']) { c_ws_plugin__s2member_utils_logs::log_entry('paypal-checkout', array( 'ppco' => 'checkout', 'env_setting' => $env_setting, 'event' => 'subscription_custom_id_mismatch', 'subscription_id' => $subscription_id, 'expected' => (string)$token['invoice'], 'actual' => $custom_id, )); echo wp_json_encode(array('error' => 'subscription_custom_id_mismatch')); exit(); } if($gateway_checkout_id) { if(in_array($status, array('APPROVAL_PENDING', 'APPROVED'), TRUE)) { //260902.0200 Coordinator-backed Pro-Forms do not treat PayPal creation/approval-pending states as paid entitlement; the browser waits briefly for ACTIVE and the activation webhook remains an off-session fallback. c_ws_plugin__s2member_gateway_checkouts::update($gateway_checkout_id, array('gateway_status' => $status)); echo wp_json_encode(array('pending_activation' => TRUE, 'subscription_id' => $subscription_id, 'status' => $status)); exit(); } if($status !== 'ACTIVE' && !$allow_expired_single_cycle) { c_ws_plugin__s2member_utils_logs::log_entry('paypal-checkout', array( 'ppco' => 'checkout', 'env_setting' => $env_setting, 'event' => 'subscription_status_invalid', 'subscription_id' => $subscription_id, 'status' => $status, )); echo wp_json_encode(array('error' => 'subscription_status_invalid')); exit(); } c_ws_plugin__s2member_gateway_checkouts::update($gateway_checkout_id, array('gateway_status' => $status)); } else if(!in_array($status, array('ACTIVE', 'APPROVED', 'APPROVAL_PENDING'), TRUE) && !$allow_expired_single_cycle) { //260902.0200 Preserve existing non-coordinator PayPal Checkout button behavior until those flows migrate onto Gateway Checkout and gain the same activation polling. //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. c_ws_plugin__s2member_utils_logs::log_entry('paypal-checkout', array( 'ppco' => 'checkout', 'env_setting' => $env_setting, 'event' => 'subscription_status_invalid', 'subscription_id' => $subscription_id, 'status' => $status, )); echo wp_json_encode(array('error' => 'subscription_status_invalid')); exit(); } $subscriber_email = !empty($subscription['subscriber']['email_address']) ? (string)$subscription['subscriber']['email_address'] : ''; $first_name = !empty($subscription['subscriber']['name']['given_name']) ? (string)$subscription['subscriber']['name']['given_name'] : ''; $last_name = !empty($subscription['subscriber']['name']['surname']) ? (string)$subscription['subscriber']['name']['surname'] : ''; $paypal = array( 'txn_type' => 'subscr_signup', 'payment_status' => 'Completed', 'subscr_gateway' => 'paypal', 'txn_id' => $subscription_id, 'subscr_id' => $subscription_id, 'subscr_baid' => $subscription_id, 'subscr_cid' => $subscription_id, 'mc_gross' => (string)$token['amount'], 'mc_currency' => strtoupper((string)$token['cc']), 'period1' => (!empty($token['tp']) && !empty($token['tt'])) ? ((string)$token['tp'].' '.strtoupper((string)$token['tt'])) : '0 D', 'mc_amount1' => (!empty($token['tp']) && !empty($token['tt'])) ? (string)$token['ta'] : '0.00', 'period3' => ((string)$token['rp'].' '.strtoupper((string)$token['rt'])), 'mc_amount3' => (string)$token['amount'], 'recurring' => ((isset($token['rr']) && (string)$token['rr'] === '1') ? '1' : '0'), 'invoice' => (string)$token['invoice'], 'custom' => (string)$token['custom'], 'item_name' => (string)$token['item_name'], 'item_number' => (string)$token['item_number'], 'payer_email' => $subscriber_email, 'first_name' => $first_name, 'last_name' => $last_name, 'option_name1' => (string)$token['on0'], 'option_selection1' => (string)$token['os0'], 'option_name2' => (string)$token['on1'], 'option_selection2' => (string)$token['os1'], ); $option_ppco_subscr = 's2m_ppco_subscr_done_'.md5($subscription_id); //260818.0603 Mark the Subscription done only after Notify succeeds, using the same lock as webhook activation fallback. $notify_result = c_ws_plugin__s2member_paypal_utilities::paypal_checkout_notify_once($paypal, $option_ppco_subscr); $notify_code = !empty($notify_result['code']) ? (int)$notify_result['code'] : 0; $notify_msg = !empty($notify_result['message']) ? (string)$notify_result['message'] : ''; $notify_body = !empty($notify_result['body']) ? (string)$notify_result['body'] : ''; if(empty($notify_result['ok'])) { c_ws_plugin__s2member_utils_logs::log_entry('paypal-checkout', array( 'ppco' => 'checkout', 'env_setting' => $env_setting, 'event' => 'notify_proxy_failed', 'subscription_id' => $subscription_id, 'code' => $notify_code, 'message' => $notify_msg, 'body' => $notify_body, 'error' => !empty($notify_result['error']) ? (string)$notify_result['error'] : 'notify_proxy_failed', )); echo wp_json_encode(array('error' => !empty($notify_result['error']) ? (string)$notify_result['error'] : 'notify_proxy_failed')); exit(); } if(!empty($notify_result['duplicate'])) c_ws_plugin__s2member_utils_logs::log_entry('paypal-checkout', array( 'ppco' => 'checkout', 'env_setting' => $env_setting, 'event' => 'duplicate_subscription_ignored', 'subscription_id' => $subscription_id, 'option' => $option_ppco_subscr, )); else { c_ws_plugin__s2member_utils_logs::log_entry('paypal-checkout', array( 'ppco' => 'checkout', 'env_setting' => $env_setting, 'event' => 'notify_proxy_response', 'subscription_id' => $subscription_id, 'code' => $notify_code, 'message' => $notify_msg, 'body' => $notify_body, )); //260818.0603 Only successful first-pass fulfillment should trigger replacement-subscription cancellation. if(!empty($notify_result['processed']) && $old__subscr_id && apply_filters('s2member_pro_cancels_old_rp_before_new_rp', ($old__subscr_id !== $subscription_id), get_defined_vars())) c_ws_plugin__s2member_utilities::cancel_gateway_subscription($old__subscr_gateway, $old__subscr_id, $old__subscr_baid, $old__subscr_cid, $old__ipn_signup_vars); } $return_url = (string)$token['return']; $return_url = add_query_arg('s2member_paypal_proxy', 'paypal', $return_url); $return_post = array_merge($paypal, array( 's2member_paypal_proxy' => 'paypal', 's2member_paypal_proxy_use' => 'paypal_checkout', )); //260817 Sign the exact browser-return payload without exposing the reusable internal PayPal proxy key. $return_handoff = c_ws_plugin__s2member_paypal_utilities::paypal_checkout_return_handoff_create($return_post); if(!$return_handoff) { if(!headers_sent()) status_header(500); echo wp_json_encode(array('error' => 'return_handoff_failed')); exit(); } $return_post['s2member_paypal_checkout_handoff'] = $return_handoff; echo wp_json_encode(array( 'rtn_url' => $return_url, 'rtn_post' => $return_post, )); exit(); } if($op === 'cancel_subscription') { if(!is_user_logged_in()) { c_ws_plugin__s2member_utils_logs::log_entry('paypal-checkout', array( 'ppco' => 'checkout', 'env_setting' => $env_setting, 'event' => 'cancel_subscription_not_logged_in', 'token' => $token, )); echo wp_json_encode(array('error' => 'not_logged_in')); exit(); } $user_id = (int)get_current_user_id(); $nonce = !empty($_POST['s2member_paypal_checkout_nonce']) ? trim(stripslashes((string)$_POST['s2member_paypal_checkout_nonce'])) : ''; if(!$nonce || !wp_verify_nonce($nonce, 's2m_ppco_cancel_'.$user_id)) { c_ws_plugin__s2member_utils_logs::log_entry('paypal-checkout', array( 'ppco' => 'checkout', 'env_setting' => $env_setting, 'event' => 'cancel_subscription_bad_nonce', 'user_id'=> $user_id, )); echo wp_json_encode(array('error' => 'bad_nonce')); exit(); } $token_user_id = !empty($token['user_id']) ? (int)$token['user_id'] : 0; $token_subscr_id = !empty($token['subscr_id']) ? (string)$token['subscr_id'] : ''; if(!$token_user_id || $token_user_id !== $user_id || !$token_subscr_id) { c_ws_plugin__s2member_utils_logs::log_entry('paypal-checkout', array( 'ppco' => 'checkout', 'env_setting' => $env_setting, 'event' => 'cancel_subscription_token_mismatch', 'user_id' => $user_id, 'token' => $token, )); echo wp_json_encode(array('error' => 'token_mismatch')); exit(); } $subscr_id = (string)get_user_option('s2member_subscr_id', $user_id); if(!$subscr_id || $subscr_id !== $token_subscr_id) { c_ws_plugin__s2member_utils_logs::log_entry('paypal-checkout', array( 'ppco' => 'checkout', 'env_setting' => $env_setting, 'event' => 'cancel_subscription_user_mismatch', 'user_id' => $user_id, 'user_subscr'=> $subscr_id, 'token_subscr'=> $token_subscr_id, )); echo wp_json_encode(array('error' => 'user_mismatch')); exit(); } $reason = !empty($_POST['reason']) ? trim(stripslashes((string)$_POST['reason'])) : 'Cancelled by subscriber.'; $reason = sanitize_text_field($reason); if(!$reason) $reason = 'Cancelled by subscriber.'; //260819.0417 Resolve the active subscription through whichever configured PayPal API family owns it. $ipn_signup_vars = get_user_option('s2member_ipn_signup_vars', $user_id); $ipn_signup_vars = (is_array($ipn_signup_vars) && !empty($ipn_signup_vars['subscr_id']) && (string)$ipn_signup_vars['subscr_id'] === (string)$subscr_id) ? $ipn_signup_vars : array(); $next_billing_time = ''; $eot = c_ws_plugin__s2member_utils_users::get_user_eot($user_id, TRUE, 'next'); if(is_array($eot) && !empty($eot['type']) && $eot['type'] === 'next' && !empty($eot['time']) && (int)$eot['time'] > time()) $next_billing_time = gmdate('Y-m-d\TH:i:s\Z', (int)$eot['time']); $cancelled = c_ws_plugin__s2member_utilities::cancel_gateway_subscription( 'paypal', $subscr_id, (string)get_user_option('s2member_subscr_baid', $user_id), (string)get_user_option('s2member_subscr_cid', $user_id), $ipn_signup_vars, TRUE, $reason ); c_ws_plugin__s2member_utils_logs::log_entry('paypal-checkout', array( 'ppco' => 'checkout', 'env_setting' => $env_setting, 'event' => 'cancel_subscription_response', 'user_id' => $user_id, 'subscr_id' => $subscr_id, 'accepted' => $cancelled ? 1 : 0, )); if($cancelled) { // Immediately feed s2Member's existing cancel handler (webhooks may be missing in MVP sites). $paypal = array( 'txn_type' => 'subscr_cancel', 'payment_status' => 'Completed', 'subscr_gateway' => 'paypal', 'txn_id' => $subscr_id, 'subscr_id' => $subscr_id, 'custom' => (string)get_user_option('s2member_custom', $user_id), // Help legacy notify logic resolve user in some fallback cases. 'mp_id' => $subscr_id, 'recurring_payment_id' => $subscr_id, //260517 Provide safe defaults when signup vars are missing. 'item_number' => (string)c_ws_plugin__s2member_user_access::user_access_level(wp_get_current_user()), 'item_name' => 'PayPal Checkout Subscription', // Best-effort payer email for logs/fallback logic. 'payer_email' => (string)wp_get_current_user()->user_email, ); //260517 Enrich with stored signup vars so legacy cancel handler can match and compute EOT. if($ipn_signup_vars) { if(!empty($ipn_signup_vars['item_number'])) $paypal['item_number'] = (string)$ipn_signup_vars['item_number']; if(!empty($ipn_signup_vars['item_name'])) $paypal['item_name'] = (string)$ipn_signup_vars['item_name']; if(empty($paypal['period1']) && !empty($ipn_signup_vars['period1'])) $paypal['period1'] = (string)$ipn_signup_vars['period1']; if(empty($paypal['period3']) && !empty($ipn_signup_vars['period3'])) $paypal['period3'] = (string)$ipn_signup_vars['period3']; } $notify_url = home_url('/?s2member_paypal_notify=1'); $notify_post = array_merge($paypal, array( 'proxy_user_id' => $user_id, //260517 'proxy_next_billing_time' => $next_billing_time, //260517 's2member_paypal_proxy' => 'paypal', 's2member_paypal_proxy_use' => 'paypal_checkout', 's2member_paypal_proxy_verification' => c_ws_plugin__s2member_paypal_utilities::paypal_proxy_key_gen(), )); $notify_r = c_ws_plugin__s2member_utils_urls::remote($notify_url, $notify_post, array('timeout' => 20), true); if(!is_array($notify_r)) $notify_r = array('code' => 0, 'message' => 'request_failed', 'body' => ''); $notify_code = !empty($notify_r['code']) ? (int)$notify_r['code'] : 0; if(!($notify_code >= 200 && $notify_code <= 299)) { c_ws_plugin__s2member_utils_logs::log_entry('paypal-checkout', array( 'ppco' => 'checkout', 'env_setting' => $env_setting, 'event' => 'cancel_subscription_notify_failed', 'user_id' => $user_id, 'subscr_id' => $subscr_id, 'notify_code' => $notify_code, 'notify_msg' => !empty($notify_r['message']) ? (string)$notify_r['message'] : '', )); } echo wp_json_encode(array('ok' => 1)); exit(); } echo wp_json_encode(array('error' => 'cancel_failed')); exit(); } if($op === 'create_order') { $order = c_ws_plugin__s2member_paypal_utilities::paypal_checkout_order_create($token); c_ws_plugin__s2member_utils_logs::log_entry('paypal-checkout', array( 'ppco' => 'checkout', 'env_setting' => $env_setting, 'event' => 'create_order_response', 'order' => $order, 'token' => $token, )); if(empty($order['id'])) { c_ws_plugin__s2member_utils_logs::log_entry('paypal-checkout', array( 'ppco' => 'checkout', 'env_setting' => $env_setting, 'event' => 'order_create_failed', 'order' => $order, 'token' => $token, )); $error = !empty($order['__error']) ? (string)$order['__error'] : 'order_create_failed'; $recoverable = ($error === 'gateway_checkout_busy'); //260902.0646 Only an overlapping request can populate a missing order ID asynchronously; an ambiguous provider create has no pre-approval webhook, so tell the customer to retry the same idempotent checkout instead of polling pointlessly. echo wp_json_encode(array('error' => $error, 'recoverable' => $recoverable, 'retryable' => ($error === 'order_create_unresolved'))); exit(); } echo wp_json_encode(array('order_id' => $order['id'])); exit(); } else if($op === 'get_order_status') { //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. $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'] : ''; $gateway_checkout = $gateway_checkout_id ? c_ws_plugin__s2member_gateway_checkouts::load_state($gateway_checkout_id) : FALSE; if(!$gateway_checkout || (string)$gateway_checkout['gateway'] !== 'paypal_checkout' || (string)$gateway_checkout['operation'] !== 'payment') { echo wp_json_encode(array('error' => 'gateway_checkout_invalid')); exit(); } $private_context = c_ws_plugin__s2member_gateway_checkouts::private_context_get($gateway_checkout_id); $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(); //260902.0635 Poll only local coordinator state while independent PayPal webhooks resolve delayed creates/captures; do not hammer the provider from the browser. echo wp_json_encode(array( 'order_id' => !empty($gateway_checkout['gateway_ids']['order_id']) ? (string)$gateway_checkout['gateway_ids']['order_id'] : '', 'capture_id' => !empty($gateway_checkout['gateway_ids']['capture_id']) ? (string)$gateway_checkout['gateway_ids']['capture_id'] : '', 'status' => !empty($gateway_checkout['gateway_status']) ? (string)$gateway_checkout['gateway_status'] : '', 'fulfillment_status' => !empty($gateway_checkout['fulfillment_status']) ? (string)$gateway_checkout['fulfillment_status'] : '', 'fulfilled' => ((string)$gateway_checkout['fulfillment_status'] === 'fulfilled' && !empty($fulfillment_result)), )); exit(); } else if($op === 'capture_order') { $order_id = !empty($_POST['order_id']) ? trim(stripslashes((string)$_POST['order_id'])) : ''; if(!$order_id) { echo wp_json_encode(array('error' => 'missing_order_id')); exit(); } $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'] : ''; if($gateway_checkout_id) { $gateway_checkout = c_ws_plugin__s2member_gateway_checkouts::load_state($gateway_checkout_id); $private_context = $gateway_checkout ? c_ws_plugin__s2member_gateway_checkouts::private_context_get($gateway_checkout_id) : FALSE; $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(); if($gateway_checkout && (string)$gateway_checkout['fulfillment_status'] === 'fulfilled' && !empty($fulfillment_result['rtn_url']) && !empty($fulfillment_result['rtn_post'])) { //260902.0646 A webhook may have finished checkout while the browser was gone; return the saved browser result locally without touching PayPal or repeating fulfillment. echo wp_json_encode(array('rtn_url' => $fulfillment_result['rtn_url'], 'rtn_post' => $fulfillment_result['rtn_post'])); exit(); } } $capture = c_ws_plugin__s2member_paypal_utilities::paypal_checkout_order_capture($order_id, $token); $cap0 = (!empty($capture['purchase_units'][0]['payments']['captures'][0]) && is_array($capture['purchase_units'][0]['payments']['captures'][0])) ? $capture['purchase_units'][0]['payments']['captures'][0] : array(); c_ws_plugin__s2member_utils_logs::log_entry('paypal-checkout', array( 'ppco' => 'checkout', 'env_setting' => $env_setting, 'event' => 'capture_response', 'order_id' => $order_id, 'status' => !empty($capture['status']) ? (string)$capture['status'] : '', 'capture_id' => !empty($cap0['id']) ? (string)$cap0['id'] : '', 'amount' => !empty($cap0['amount']['value']) ? (string)$cap0['amount']['value'] : '', 'cc' => !empty($cap0['amount']['currency_code']) ? (string)$cap0['amount']['currency_code'] : '', 'payer' => !empty($capture['payer']['email_address']) ? (string)$capture['payer']['email_address'] : '', 'capture' => $capture, 'token' => $token, )); if($gateway_checkout_id) { if(!empty($capture['__error'])) { $error = (string)$capture['__error']; $recoverable = in_array($error, array('capture_pending', 'order_capture_unresolved', 'gateway_checkout_busy'), TRUE); echo wp_json_encode(array('error' => $error, 'recoverable' => $recoverable, 'pending' => ($error === 'capture_pending'))); exit(); } $fulfillment = c_ws_plugin__s2member_paypal_utilities::paypal_checkout_order_fulfill($capture, $token); if(empty($fulfillment['ok']) || empty($fulfillment['rtn_url']) || empty($fulfillment['rtn_post'])) { echo wp_json_encode(array('error' => !empty($fulfillment['error']) ? (string)$fulfillment['error'] : 'order_fulfillment_failed')); exit(); } echo wp_json_encode(array('rtn_url' => $fulfillment['rtn_url'], 'rtn_post' => $fulfillment['rtn_post'])); exit(); } if(!empty($capture['__error'])) { echo wp_json_encode(array('error' => (string)$capture['__error'])); exit(); } if(empty($capture['status']) || strtoupper($capture['status']) !== 'COMPLETED') { echo wp_json_encode(array('error' => 'order_capture_failed')); exit(); } /* * Build PayPal-like variables to feed s2Member's existing IPN + Return handlers. */ $payer_email = !empty($capture['payer']['email_address']) ? (string)$capture['payer']['email_address'] : ''; $first_name = !empty($capture['payer']['name']['given_name']) ? (string)$capture['payer']['name']['given_name'] : ''; $last_name = !empty($capture['payer']['name']['surname']) ? (string)$capture['payer']['name']['surname'] : ''; $pu_amount = !empty($capture['purchase_units'][0]['payments']['captures'][0]['amount']['value']) ? (string)$capture['purchase_units'][0]['payments']['captures'][0]['amount']['value'] : ''; $pu_cc = !empty($capture['purchase_units'][0]['payments']['captures'][0]['amount']['currency_code']) ? (string)$capture['purchase_units'][0]['payments']['captures'][0]['amount']['currency_code'] : ''; $pu_cap_id = !empty($capture['purchase_units'][0]['payments']['captures'][0]['id']) ? (string)$capture['purchase_units'][0]['payments']['captures'][0]['id'] : ''; if(!$payer_email || !$pu_amount || !$pu_cc || !$pu_cap_id) { c_ws_plugin__s2member_utils_logs::log_entry('paypal-checkout', array( 'ppco' => 'checkout', 'env_setting' => $env_setting, 'event' => 'capture_missing_fields', 'order_id' => $order_id, 'capture' => $capture, 'token' => $token, )); echo wp_json_encode(array('error' => 'capture_missing_fields')); exit(); } // Extra safety: enforce token matches amount/currency/invoice/custom if provided. //260228 Normalize amount strings before comparison (e.g. 20 vs 20.00). if(!empty($token['amount']) && number_format((float)$token['amount'], 2, '.', '') !== number_format((float)$pu_amount, 2, '.', '')) { c_ws_plugin__s2member_utils_logs::log_entry('paypal-checkout', array( 'ppco' => 'checkout', 'env_setting' => $env_setting, 'event' => 'amount_mismatch', 'order_id' => $order_id, 'token' => $token, 'pu' => array('amount' => $pu_amount, 'cc' => $pu_cc), )); echo wp_json_encode(array('error' => 'amount_mismatch')); exit(); } if(!empty($token['cc']) && strtoupper((string)$token['cc']) !== strtoupper((string)$pu_cc)) { c_ws_plugin__s2member_utils_logs::log_entry('paypal-checkout', array( 'ppco' => 'checkout', 'env_setting' => $env_setting, 'event' => 'currency_mismatch', 'order_id' => $order_id, 'token' => $token, 'pu' => array('amount' => $pu_amount, 'cc' => $pu_cc), )); echo wp_json_encode(array('error' => 'currency_mismatch')); exit(); } $cap_invoice_id = ''; if(!empty($capture['purchase_units'][0]['invoice_id'])) $cap_invoice_id = (string)$capture['purchase_units'][0]['invoice_id']; else if(!empty($capture['purchase_units'][0]['payments']['captures'][0]['invoice_id'])) $cap_invoice_id = (string)$capture['purchase_units'][0]['payments']['captures'][0]['invoice_id']; if($cap_invoice_id && $cap_invoice_id !== (string)$token['invoice']) { c_ws_plugin__s2member_utils_logs::log_entry('paypal-checkout', array( 'ppco' => 'checkout', 'env_setting' => $env_setting, 'event' => 'invoice_mismatch', 'order_id' => $order_id, 'token' => $token, 'invoice' => $cap_invoice_id, )); echo wp_json_encode(array('error' => 'invoice_mismatch')); exit(); } $cap_custom_id = ''; if(!empty($capture['purchase_units'][0]['custom_id'])) $cap_custom_id = (string)$capture['purchase_units'][0]['custom_id']; else if(!empty($capture['purchase_units'][0]['payments']['captures'][0]['custom_id'])) $cap_custom_id = (string)$capture['purchase_units'][0]['payments']['captures'][0]['custom_id']; if($cap_custom_id && !empty($token['custom']) && $cap_custom_id !== (string)$token['custom']) { c_ws_plugin__s2member_utils_logs::log_entry('paypal-checkout', array( 'ppco' => 'checkout', 'env_setting' => $env_setting, 'event' => 'custom_mismatch', 'order_id' => $order_id, 'token' => $token, 'custom' => array( 'token' => !empty($token['custom']) ? $token['custom'] : '', 'paypal' => $cap_custom_id, ), )); echo wp_json_encode(array('error' => 'custom_mismatch')); exit(); } $paypal = array( 'txn_type' => 'web_accept', 'payment_status' => 'Completed', 'subscr_gateway' => 'paypal', 'txn_id' => $pu_cap_id, 'subscr_id' => $pu_cap_id, 'subscr_baid' => $pu_cap_id, 'subscr_cid' => $pu_cap_id, 'mc_gross' => $pu_amount, 'mc_currency' => strtoupper($pu_cc), 'invoice' => (string)$token['invoice'], 'custom' => (string)$token['custom'], 'item_name' => (string)$token['item_name'], 'item_number' => (string)$token['item_number'], 'payer_email' => $payer_email, 'first_name' => $first_name, 'last_name' => $last_name, // Preserve s2Member's tracking option fields. 'option_name1' => (string)$token['on0'], 'option_selection1' => (string)$token['os0'], 'option_name2' => (string)$token['on1'], 'option_selection2' => (string)$token['os1'], ); //260827.0051 Keep AJAX capture fulfillment aligned with the redirect capture path so Pro-Form tax, email/coupon routing, and resolved success URLs survive the shared Framework handler. if(isset($token['tax'])) $paypal['tax'] = (string)$token['tax']; $is_independent_ccaps_sale = (strpos((string)$token['item_number'], '*:') === 0); $is_specific_post_page_sale = (strpos((string)$token['item_number'], 'sp:') === 0); $can_cancel_old_subscr = (!$is_independent_ccaps_sale && !$is_specific_post_page_sale); //260407 Only membership replacement-style PPCO purchases should cancel an existing recurring subscription here. $proxy_use = !empty($token['s2member_paypal_proxy_use']) ? (string)$token['s2member_paypal_proxy_use'] : 'paypal_checkout'; $notify_extra = array(); if(!empty($token['s2member_paypal_proxy_coupon']) && is_array($token['s2member_paypal_proxy_coupon'])) $notify_extra['s2member_paypal_proxy_coupon'] = $token['s2member_paypal_proxy_coupon']; if(array_key_exists('s2member_paypal_proxy_return_url', $token)) $notify_extra['s2member_paypal_proxy_return_url'] = (string)$token['s2member_paypal_proxy_return_url']; $notify_done_option = 's2m_ppco_capture_done_'.md5($pu_cap_id); $notify_result = c_ws_plugin__s2member_paypal_utilities::paypal_checkout_notify_once($paypal, $notify_done_option, $proxy_use, $notify_extra); if(empty($notify_result['ok'])) { c_ws_plugin__s2member_utils_logs::log_entry('paypal-checkout', array( 'ppco' => 'checkout', 'env_setting' => $env_setting, 'event' => 'notify_proxy_failed', 'order_id' => $order_id, 'txn_id' => $pu_cap_id, 'code' => !empty($notify_result['code']) ? (int)$notify_result['code'] : 0, 'message' => !empty($notify_result['message']) ? (string)$notify_result['message'] : '', 'body' => !empty($notify_result['body']) ? (string)$notify_result['body'] : '', )); echo wp_json_encode(array('error' => !empty($notify_result['error']) ? (string)$notify_result['error'] : 'notify_proxy_failed')); exit(); } if(!empty($notify_result['processed'])) { c_ws_plugin__s2member_utils_logs::log_entry('paypal-checkout', array( 'ppco' => 'checkout', 'env_setting' => $env_setting, 'event' => 'notify_proxy_response', 'order_id' => $order_id, 'txn_id' => $pu_cap_id, 'code' => !empty($notify_result['code']) ? (int)$notify_result['code'] : 0, 'message' => !empty($notify_result['message']) ? (string)$notify_result['message'] : '', 'body' => !empty($notify_result['body']) ? (string)$notify_result['body'] : '', )); //260407 Framework PPCO AJAX replacements can also replace subscriptions created by other gateways without affecting independent CCAPS or specific post/page purchases. if($can_cancel_old_subscr && $old__subscr_id && apply_filters('s2member_pro_cancels_old_rp_before_new_rp', ($old__subscr_id !== $pu_cap_id), get_defined_vars())) //260406 c_ws_plugin__s2member_utilities::cancel_gateway_subscription($old__subscr_gateway, $old__subscr_id, $old__subscr_baid, $old__subscr_cid, $old__ipn_signup_vars); //260407 } // 2) Send the user through the existing Return handler via POST (sets cookies, thank-you UX, reg tokens, etc). $return_url = (string)$token['return']; $return_url = add_query_arg('s2member_paypal_proxy', 'paypal', $return_url); $return_post = array_merge($paypal, array( 's2member_paypal_proxy' => 'paypal', 's2member_paypal_proxy_use' => $proxy_use, )); //260827.0051 Carry the Pro-Form's resolved success URL inside the signed browser return; Specific Post/Page uses the Notify response body for its generated access URL. if(array_key_exists('s2member_paypal_proxy_return_url', $token)) $return_post['s2member_paypal_proxy_return_url'] = !empty($notify_result['body']) ? trim((string)$notify_result['body']) : ''; //260817 Sign the exact browser-return payload without exposing the reusable internal PayPal proxy key. $return_handoff = c_ws_plugin__s2member_paypal_utilities::paypal_checkout_return_handoff_create($return_post); if(!$return_handoff) { if(!headers_sent()) status_header(500); echo wp_json_encode(array('error' => 'return_handoff_failed')); exit(); } $return_post['s2member_paypal_checkout_handoff'] = $return_handoff; echo wp_json_encode(array( 'rtn_url' => $return_url, 'rtn_post' => $return_post, )); exit(); } echo wp_json_encode(array('error' => 'unknown_op')); exit(); } } }