/** * Processes gateway IPN return. * * @since 1.0.0 * @param MS_Model_Transactionlog $log Optional. A transaction log item * that will be updated instead of creating a new log entry. */ public function handle_return($log = false) { $success = false; $exit = false; $redirect = false; $notes = ''; $status = null; $external_id = null; $invoice_id = 0; $subscription_id = 0; $amount = 0; if (!empty($_POST['vendor_order_id']) && !empty($_POST['md5_hash'])) { $invoice_id = intval($_POST['vendor_order_id']); $invoice = MS_Factory::load('MS_Model_Invoice', $invoice_id); $raw_hash = $_POST['sale_id'] . $this->seller_id . $_POST['invoice_id'] . $this->secret_word; $md5_hash = strtoupper(md5($raw_hash)); if ($md5_hash == $_POST['md5_hash'] && !empty($_POST['message_type']) && ($invoice->id = $invoice_id)) { $subscription = $invoice->get_subscription(); $membership = $subscription->get_membership(); $member = $subscription->get_member(); $subscription_id = $subscription->id; $external_id = $_POST['invoice_id']; $amount = (double) $_POST['invoice_list_amount']; switch ($_POST['message_type']) { case 'RECURRING_INSTALLMENT_SUCCESS': $notes = 'Payment received'; // Not sure if the invoice was already paid via the // INVOICE_STATUS_CHANGED message if (!$invoice->is_paid()) { $success = true; } break; case 'INVOICE_STATUS_CHANGED': $notes = sprintf('Invoice was %s', $_POST['invoice_status']); switch ($_POST['invoice_status']) { case 'deposited': // Not sure if invoice was already paid via the // RECURRING_INSTALLMENT_SUCCESS message. if (!$invoice->is_paid()) { $success = true; } break; case 'declied': $status = MS_Model_Invoice::STATUS_DENIED; break; } break; case 'RECURRING_STOPPED': $notes = 'Recurring payments stopped manually'; $member->cancel_membership($membership->id); $member->save(); break; case 'FRAUD_STATUS_CHANGED': $notes = 'Ignored: Users Fraud-status was checked'; $success = null; break; case 'ORDER_CREATED': $notes = 'Ignored: 2Checkout created a new order'; $success = null; break; case 'RECURRING_RESTARTED': $notes = 'Ignored: Recurring payments started'; $success = null; break; case 'RECURRING_COMPLETE': $notes = 'Ignored: Recurring complete'; $success = null; break; case 'RECURRING_INSTALLMENT_FAILED': $notes = 'Ignored: Recurring payment failed'; $success = null; $status = MS_Model_Invoice::STATUS_PENDING; break; default: $notes = sprintf('Warning: Unclear command "%s"', $_POST['message_type']); break; } $invoice->add_notes($notes); $invoice->save(); if ($success) { $invoice->pay_it($this->id, $external_id); } elseif (!empty($status)) { $invoice->status = $status; $invoice->save(); $invoice->changed(); } do_action('ms_gateway_2checkout_payment_processed_' . $status, $invoice, $subscription); } else { $reason = 'Unexpected transaction response'; switch (true) { case $md5_hash != $_POST['md5_hash']: $reason = 'MD5 Hash invalid'; break; case empty($_POST['message_type']): $reason = 'Message type is empty'; break; case $invoice->id != $invoice_id: $reason = sprintf('Expected invoice_id "%s" but got "%s"', $invoice->id, $invoice_id); break; } $notes = 'Response Error: ' . $reason; MS_Helper_Debug::log($notes); MS_Helper_Debug::log($response); $exit = true; } } else { // Did not find expected POST variables. Possible access attempt from a non PayPal site. $notes = 'Error: Missing POST variables. Identification is not possible.'; MS_Helper_Debug::log($notes); $redirect = MS_Helper_Utility::home_url('/'); $exit = true; } if (!$log) { do_action('ms_gateway_transaction_log', self::ID, 'handle', $success, $subscription_id, $invoice_id, $amount, $notes, $external_id); if ($redirect) { wp_safe_redirect($redirect); exit; } if ($exit) { exit; } } else { $log->invoice_id = $invoice_id; $log->subscription_id = $subscription_id; $log->amount = $amount; $log->description = $notes; $log->external_id = $external_id; if ($success) { $log->manual_state('ok'); } $log->save(); } do_action('ms_gateway_2checkout_handle_return_after', $this); if ($log) { return $log; } }
/** * Processes gateway IPN return. * * @since 1.0.0 * @param MS_Model_Transactionlog $log Optional. A transaction log item * that will be updated instead of creating a new log entry. */ public function handle_return($log = false) { $success = false; $exit = false; $redirect = false; $notes = ''; $status = null; $invoice_id = 0; $subscription_id = 0; $amount = 0; do_action('ms_gateway_paypalsingle_handle_return_before', $this); lib3()->array->strip_slashes($_POST, 'pending_reason'); if ((isset($_POST['payment_status']) || isset($_POST['txn_type'])) && !empty($_POST['invoice'])) { if ($this->is_live_mode()) { $domain = 'https://www.paypal.com'; } else { $domain = 'https://www.sandbox.paypal.com'; } // Ask PayPal to validate our $_POST data. $ipn_data = (array) stripslashes_deep($_POST); $ipn_data['cmd'] = '_notify-validate'; $response = wp_remote_post($domain . '/cgi-bin/webscr', array('timeout' => 60, 'sslverify' => false, 'httpversion' => '1.1', 'body' => $ipn_data)); $invoice_id = intval($_POST['invoice']); $external_id = $_POST['txn_id']; $amount = (double) $_POST['mc_gross']; $currency = $_POST['mc_currency']; $invoice = MS_Factory::load('MS_Model_Invoice', $invoice_id); if (!is_wp_error($response) && !MS_Model_Transactionlog::was_processed(self::ID, $external_id) && 200 == $response['response']['code'] && !empty($response['body']) && 'VERIFIED' == $response['body'] && $invoice->id == $invoice_id) { $new_status = false; $subscription = $invoice->get_subscription(); $membership = $subscription->get_membership(); $member = $subscription->get_member(); $subscription_id = $subscription->id; // Process PayPal response switch ($_POST['payment_status']) { // Successful payment case 'Completed': case 'Processed': $success = true; if ($amount == $invoice->total) { $notes .= __('Payment successful', 'membership2'); } else { $notes .= __('Payment registered, though amount differs from invoice.', 'membership2'); } break; case 'Reversed': $notes = __('Last transaction has been reversed. Reason: Payment has been reversed (charge back). ', 'membership2'); $status = MS_Model_Invoice::STATUS_DENIED; break; case 'Refunded': $notes = __('Last transaction has been reversed. Reason: Payment has been refunded', 'membership2'); $status = MS_Model_Invoice::STATUS_DENIED; break; case 'Denied': $notes = __('Last transaction has been reversed. Reason: Payment Denied', 'membership2'); $status = MS_Model_Invoice::STATUS_DENIED; break; case 'Pending': $pending_str = array('address' => __('Customer did not include a confirmed shipping address', 'membership2'), 'authorization' => __('Funds not captured yet', 'membership2'), 'echeck' => __('eCheck that has not cleared yet', 'membership2'), 'intl' => __('Payment waiting for aproval by service provider', 'membership2'), 'multi-currency' => __('Payment waiting for service provider to handle multi-currency process', 'membership2'), 'unilateral' => __('Customer did not register or confirm his/her email yet', 'membership2'), 'upgrade' => __('Waiting for service provider to upgrade the PayPal account', 'membership2'), 'verify' => __('Waiting for service provider to verify his/her PayPal account', 'membership2'), '*' => ''); $reason = $_POST['pending_reason']; $notes = __('Last transaction is pending. Reason: ', 'membership2') . (isset($pending_str[$reason]) ? $pending_str[$reason] : $pending_str['*']); $status = MS_Model_Invoice::STATUS_PENDING; break; default: case 'Partially-Refunded': case 'In-Progress': $success = null; break; } if ('new_case' == $_POST['txn_type'] && 'dispute' == $_POST['case_type']) { // Status: Dispute $status = MS_Model_Invoice::STATUS_DENIED; $notes = __('Dispute about this payment', 'membership2'); } if (!empty($notes)) { $invoice->add_notes($notes); } $invoice->save(); if ($success) { $invoice->pay_it($this->id, $external_id); } elseif (!empty($status)) { $invoice->status = $status; $invoice->save(); $invoice->changed(); } do_action('ms_gateway_paypalsingle_payment_processed_' . $status, $invoice, $subscription); } else { $reason = 'Unexpected transaction response'; switch (true) { case is_wp_error($response): $reason = 'Response is error'; break; case 200 != $response['response']['code']: $reason = 'Response code is ' . $response['response']['code']; break; case empty($response['body']): $reason = 'Response is empty'; break; case 'VERIFIED' != $response['body']: $reason = sprintf('Expected response "%s" but got "%s"', 'VERIFIED', (string) $response['body']); break; case $invoice->id != $invoice_id: $reason = sprintf('Expected invoice_id "%s" but got "%s"', $invoice->id, $invoice_id); break; case MS_Model_Transactionlog::was_processed(self::ID, $external_id): $reason = 'Duplicate: Already processed that transaction.'; break; } $notes = 'Response Error: ' . $reason; $exit = true; } } else { // Did not find expected POST variables. Possible access attempt from a non PayPal site. $u_agent = $_SERVER['HTTP_USER_AGENT']; if (false === strpos($u_agent, 'PayPal')) { // Very likely someone tried to open the URL manually. Redirect to home page $notes = 'Error: Missing POST variables. Redirect user to Home-URL.'; $redirect = MS_Helper_Utility::home_url('/'); } else { $notes = 'Error: Missing POST variables. Identification is not possible.'; } $exit = true; } if (!$log) { do_action('ms_gateway_transaction_log', self::ID, 'handle', $success, $subscription_id, $invoice_id, $amount, $notes, $external_id); if ($redirect) { wp_safe_redirect($redirect); exit; } if ($exit) { exit; } } else { $log->invoice_id = $invoice_id; $log->subscription_id = $subscription_id; $log->amount = $amount; $log->description = $notes; $log->external_id = $external_id; if ($success) { $log->manual_state('ok'); } $log->save(); } do_action('ms_gateway_paypalsingle_handle_return_after', $this, $log); if ($log) { return $log; } }
/** * Processes gateway IPN return. * * @since 1.0.0 * @param MS_Model_Transactionlog $log Optional. A transaction log item * that will be updated instead of creating a new log entry. */ public function handle_return($log = false) { $success = false; $ignore = false; $exit = false; $redirect = false; $notes = ''; $status = null; $notes_pay = ''; $notes_txn = ''; $external_id = null; $invoice_id = 0; $subscription_id = 0; $amount = 0; $transaction_type = ''; $payment_status = ''; $ext_type = false; if (!empty($_POST['txn_type'])) { $transaction_type = strtolower($_POST['txn_type']); } if (isset($_POST['mc_gross'])) { $amount = (double) $_POST['mc_gross']; } elseif (isset($_POST['mc_amount3'])) { // mc_amount1 and mc_amount2 are for trial period prices. $amount = (double) $_POST['mc_amount3']; } if (!empty($_POST['payment_status'])) { $payment_status = strtolower($_POST['payment_status']); } if (!empty($_POST['txn_id'])) { $external_id = $_POST['txn_id']; } if (!empty($_POST['mc_currency'])) { $currency = $_POST['mc_currency']; } // Step 1: Find the invoice_id and determine if payment is M2 or M1. if ($payment_status || $transaction_type) { if (!empty($_POST['invoice'])) { // BEST CASE: // 'invoice' is set in all regular M2 subscriptions! $invoice_id = intval($_POST['invoice']); /* * PayPal only knows the first invoice of the subscription. * So we need to check: If the invoice is already paid then the * payment is for a follow-up invoice. */ $invoice = MS_Factory::load('MS_Model_Invoice', $invoice_id); if ($invoice->is_paid()) { $subscription = $invoice->get_subscription(); $invoice_id = $subscription->first_unpaid_invoice(); } } elseif (!empty($_POST['custom'])) { // FALLBACK A: // Maybe it's an imported M1 subscription. $infos = explode(':', $_POST['custom']); if (count($infos) > 2) { // $infos should contain [timestamp, user_id, sub_id, key] $m1_user_id = intval($infos[1]); $m1_sub_id = intval($infos[2]); // Roughtly equals M2 membership->id. // M1 payments use the following type/status values. $pay_types = array('subscr_signup', 'subscr_payment'); $pay_stati = array('completed', 'processed'); if ($m1_user_id > 0 && $m1_sub_id > 0) { if (in_array($transaction_type, $pay_types)) { $ext_type = 'm1'; } elseif (in_array($payment_status, $pay_stati)) { $ext_type = 'm1'; } } if ('m1' == $ext_type) { $is_linked = false; // Seems to be a valid M1 payment: // Find the associated imported subscription! $subscription = MS_Model_Import::find_subscription($m1_user_id, $m1_sub_id, 'source', self::ID); if (!$subscription) { $membership = MS_Model_Import::membership_by_source($m1_sub_id); if ($membership) { $is_linked = true; $notes = sprintf('Error: User is not subscribed to Membership %s.', $membership->id); } } $invoice_id = $subscription->first_unpaid_invoice(); if (!$is_linked && !$invoice_id) { MS_Model_Import::need_matching($m1_sub_id, 'm1'); } } // end if: 'm1' == $ext_type } } elseif (!empty($_POST['btn_id']) && !empty($_POST['payer_email'])) { // FALLBACK B: // Payment was made by a custom PayPal Payment button. $user = get_user_by('email', $_POST['payer_email']); if ($user && $user->ID) { $ext_type = 'pay_btn'; $is_linked = false; $subscription = MS_Model_Import::find_subscription($user->ID, $_POST['btn_id'], 'pay_btn', self::ID); if (!$subscription) { $membership = MS_Model_Import::membership_by_matching('pay_btn', $_POST['btn_id']); if ($membership) { $is_linked = true; $notes = sprintf('Error: User is not subscribed to Membership %s.', $membership->id); } } $invoice_id = $subscription->first_unpaid_invoice(); if (!$is_linked && !$invoice_id) { MS_Model_Import::need_matching($_POST['btn_id'], 'pay_btn'); } } else { $notes = sprintf('Error: Could not find user "%s".', $_POST['payer_email']); } // end if: 'pay_btn' == $ext_type } } // Step 2a: Check if the txn_id was already processed by M2. if (MS_Model_Transactionlog::was_processed(self::ID, $external_id)) { $notes = 'Duplicate: Already processed that transaction.'; $success = false; $ignore = true; } elseif ($invoice_id) { if ($this->is_live_mode()) { $domain = 'https://www.paypal.com'; } else { $domain = 'https://www.sandbox.paypal.com'; } // PayPal post authenticity verification. $ipn_data = (array) stripslashes_deep($_POST); $ipn_data['cmd'] = '_notify-validate'; $response = wp_remote_post($domain . '/cgi-bin/webscr', array('timeout' => 60, 'sslverify' => false, 'httpversion' => '1.1', 'body' => $ipn_data)); $invoice = MS_Factory::load('MS_Model_Invoice', $invoice_id); if (!is_wp_error($response) && 200 == $response['response']['code'] && !empty($response['body']) && 'VERIFIED' == $response['body'] && $invoice->id == $invoice_id) { $subscription = $invoice->get_subscription(); $membership = $subscription->get_membership(); $member = $subscription->get_member(); $subscription_id = $subscription->id; // Process PayPal payment status if ($payment_status) { switch ($payment_status) { // Successful payment case 'completed': case 'processed': $success = true; if ($amount == $invoice->total) { $notes .= __('Payment successful', 'membership2'); } else { $notes .= __('Payment registered, though amount differs from invoice.', 'membership2'); } break; case 'reversed': $notes_pay = __('Last transaction has been reversed. Reason: Payment has been reversed (charge back).', 'membership2'); $status = MS_Model_Invoice::STATUS_DENIED; $ignore = true; break; case 'refunded': $notes_pay = __('Last transaction has been reversed. Reason: Payment has been refunded.', 'membership2'); $status = MS_Model_Invoice::STATUS_DENIED; $ignore = true; break; case 'denied': $notes_pay = __('Last transaction has been reversed. Reason: Payment Denied.', 'membership2'); $status = MS_Model_Invoice::STATUS_DENIED; $ignore = true; break; case 'pending': lib3()->array->strip_slashes($_POST, 'pending_reason'); $notes_pay = __('Last transaction is pending.', 'membership2') . ' '; switch ($_POST['pending_reason']) { case 'address': $notes_pay .= __('Customer did not include a confirmed shipping address', 'membership2'); break; case 'authorization': $notes_pay .= __('Funds not captured yet', 'membership2'); break; case 'echeck': $notes_pay .= __('The eCheck has not cleared yet', 'membership2'); break; case 'intl': $notes_pay .= __('Payment waiting for approval by service provider', 'membership2'); break; case 'multi-currency': $notes_pay .= __('Payment waiting for service provider to handle multi-currency process', 'membership2'); break; case 'unilateral': $notes_pay .= __('Customer did not register or confirm his/her email yet', 'membership2'); break; case 'upgrade': $notes_pay .= __('Waiting for service provider to upgrade the PayPal account', 'membership2'); break; case 'verify': $notes_pay .= __('Waiting for service provider to verify his/her PayPal account', 'membership2'); break; default: $notes_pay .= __('Unknown reason', 'membership2'); break; } $status = MS_Model_Invoice::STATUS_PENDING; $ignore = true; break; default: case 'partially-refunded': case 'in-progress': $notes_pay = sprintf(__('Not handling payment_status: %s', 'membership2'), $payment_status); $ignore = true; break; } } // Check for subscription details if ($transaction_type) { switch ($transaction_type) { case 'subscr_signup': case 'subscr_payment': // Payment was received $notes_txn = __('PayPal Subscripton has been created.', 'membership2'); if (0 == $invoice->total) { $success = true; } else { $ignore = true; } break; case 'subscr_modify': // Payment profile was modified $notes_txn = __('PayPal Subscription has been modified.', 'membership2'); $ignore = true; break; case 'recurring_payment_profile_canceled': case 'subscr_cancel': // Subscription was manually cancelled. $notes_txn = __('PayPal Subscription has been canceled.', 'membership2'); $member->cancel_membership($membership->id); $member->save(); $ignore = true; break; case 'recurring_payment_suspended': // Recurring subscription was manually suspended. $notes_txn = __('PayPal Subscription has been suspended.', 'membership2'); $member->cancel_membership($membership->id); $member->save(); $ignore = true; break; case 'recurring_payment_suspended_due_to_max_failed_payment': // Recurring subscription was automatically suspended. $notes_txn = __('PayPal Subscription has failed.', 'membership2'); $member->cancel_membership($membership->id); $member->save(); $ignore = true; break; case 'new_case': // New Dispute was filed for a payment. $status = MS_Model_Invoice::STATUS_DENIED; $ignore = true; break; case 'subscr_eot': /* * Meaning: Subscription expired. * * - after a one-time payment was made * - after last transaction in a recurring subscription * - payment failed * - ... * * We do not handle this event... * * One time payment sends 3 messages: * 1. subscr_start (new subscription starts) * 2. subscr_payment (payment confirmed) * 3. subscr_eot (subscription ends) */ $notes_txn = __('No more payments will be made for this subscription.', 'membership2'); $ignore = true; break; default: // Other event that we do not have a case for... $notes_txn = sprintf(__('Not handling txn_type: %s', 'membership2'), $transaction_type); $ignore = true; break; } } if (!empty($notes_pay)) { $invoice->add_notes($notes_pay); } if (!empty($notes_txn)) { $invoice->add_notes($notes_txn); } if ($notes_pay) { $notes .= ($notes ? ' | ' : '') . $notes_pay; } if ($notes_txn) { $notes .= ($notes ? ' | ' : '') . $notes_txn; } $invoice->save(); if ($success) { $invoice->pay_it($this->id, $external_id); } elseif (!empty($status)) { $invoice->status = $status; $invoice->save(); $invoice->changed(); } do_action('ms_gateway_paypalstandard_payment_processed_' . $status, $invoice, $subscription); } else { $reason = 'Unexpected transaction response'; switch (true) { case is_wp_error($response): $reason = 'PayPal did not verify this transaction: Unknown error'; break; case 200 != $response['response']['code']: $reason = sprintf('PayPal did not verify the transaction: Code %s', $response['response']['code']); break; case empty($response['body']): $reason = 'PayPal did not verify this transaction: Empty response'; break; case 'VERIFIED' != $response['body']: $reason = sprintf('PayPal did not verify this transaction: "%s"', $response['body']); break; case !$invoice->id: $reason = sprintf('Specified invoice does not exist: "%s"', $invoice_id); break; } $notes = 'Response Error: ' . $reason; $exit = true; } } else { // Did not find expected POST variables. Possible access attempt from a non PayPal site. $u_agent = $_SERVER['HTTP_USER_AGENT']; if (!$log && false === strpos($u_agent, 'PayPal')) { // Very likely someone tried to open the URL manually. Redirect to home page if (!$notes) { $notes = 'Ignored: Missing POST variables. Redirect to Home-URL.'; } $redirect = MS_Helper_Utility::home_url('/'); $ignore = true; $success = false; } elseif ('m1' == $ext_type) { /* * The payment belongs to an imported M1 subscription and could * not be auto-matched. * Do not return an error code, but also do not modify any * invoice/subscription. */ $notes = 'M1 Payment detected. Manual matching required.'; $ignore = false; $success = false; } elseif ('pay_btn' == $ext_type) { /* * The payment was made by a PayPal Payment button that was * created in the PayPal account and not by M1/M2. */ $notes = 'PayPal Payment button detected. Manual matching required.'; $ignore = false; $success = false; } else { // PayPal sent us a IPN notice about a non-Membership payment: // Ignore it, but add it to the logs. if (!empty($notes)) { // We already have an error message, do nothing. } elseif (!$payment_status || !$transaction_type) { $notes = 'Ignored: Payment_status or txn_type not specified. Cannot process.'; } elseif (empty($_POST['invoice']) && empty($_POST['custom'])) { $notes = 'Ignored: No invoice or custom data specified.'; } else { $notes = 'Ignored: Missing POST variables. Identification is not possible.'; } $ignore = true; $success = false; } $exit = true; } if ($ignore && !$success) { $success = null; $notes .= ' [Irrelevant IPN call]'; } if (!$log) { do_action('ms_gateway_transaction_log', self::ID, 'handle', $success, $subscription_id, $invoice_id, $amount, $notes, $external_id); if ($redirect) { wp_safe_redirect($redirect); exit; } if ($exit) { exit; } } else { $log->invoice_id = $invoice_id; $log->subscription_id = $subscription_id; $log->amount = $amount; $log->description = $notes; $log->external_id = $external_id; if ($success) { $log->manual_state('ok'); } elseif ($ignore) { $log->manual_state('ignore'); } $log->save(); } do_action('ms_gateway_paypalstandard_handle_return_after', $this); if ($log) { return $log; } }