/** * Retries to process a single error-state transaction. * * Expected JSON output: * { * @var bool `success` * @var array `data` { * @var string `desc` * @var string `status` * } * } * * @since 1.0.1.2 */ public function ajax_action_retry() { if (!$this->is_admin_user()) { return; } $fields_retry = array('id'); // Save details of a single invoice. if ($this->verify_nonce() && self::validate_required($fields_retry)) { $log_id = intval($_POST['id']); MS_Model_Import::retry_to_process($log_id); $log = MS_Factory::load('MS_Model_Transactionlog', $log_id); wp_send_json_success(array('desc' => $log->description, 'state' => $log->state)); } wp_send_json_error(array('desc' => '', 'status' => '')); exit; }
/** * Custom display function to hide item table for invalid filter options. * * @since 1.0.1.2 */ public function display() { if (MS_Model_Import::can_match($this->matching_type_id, $this->matching_type)) { parent::display(); } }
/** * Create view output. * * @since 1.0.0 * * @return string */ public function to_html() { $this->check_simulation(); $buttons = array(); $module = 'billing'; if (isset($_GET['show'])) { $module = $_GET['show']; } if (!$module) { // Show a message if there are error-state transactions. $args = array('state' => 'err'); $error_count = MS_Model_Transactionlog::get_item_count($args); if ($error_count) { if (1 == $error_count) { $message = __('One transaction failed. Please %2$sreview the logs%3$s and decide if you want to ignore the transaction or manually assign it to an invoice.', 'membership2'); } else { $message = __('%1$s transactions failed. Please %2$sreview the logs%3$s and decide if you want to ignore the transaction or manually assign it to an invoice.', 'membership2'); } $review_url = MS_Controller_Plugin::get_admin_url('billing', array('show' => 'logs', 'state' => 'err')); lib3()->ui->admin_message(sprintf($message, $error_count, '<a href="' . $review_url . '">', '</a>'), 'err'); } } // Decide which list to display in the Billings page. switch ($module) { // Transaction logs. case 'logs': $title = __('Transaction Logs', 'membership2'); $listview = MS_Factory::create('MS_Helper_ListTable_TransactionLog'); $listview->prepare_items(); $buttons[] = array('type' => MS_Helper_Html::TYPE_HTML_LINK, 'url' => MS_Controller_Plugin::get_admin_url('billing'), 'value' => __('Show Invoices', 'membership2'), 'class' => 'button'); break; // M1 Migration matching. // M1 Migration matching. case 'matching': $title = __('Automatic Transaction Matching', 'membership2'); $listview = MS_Factory::create('MS_Helper_ListTable_TransactionMatching'); $listview->prepare_items(); $buttons[] = array('type' => MS_Helper_Html::TYPE_HTML_LINK, 'url' => MS_Controller_Plugin::get_admin_url('billing'), 'value' => __('Show Invoices', 'membership2'), 'class' => 'button'); $buttons[] = array('type' => MS_Helper_Html::TYPE_HTML_LINK, 'url' => MS_Controller_Plugin::get_admin_url('billing', array('show' => 'logs')), 'value' => __('Show Transaction Logs', 'membership2'), 'class' => 'button'); break; // Default billings list. // Default billings list. case 'billing': default: $title = __('Billing', 'membership2'); $listview = MS_Factory::create('MS_Helper_ListTable_Billing'); $listview->prepare_items(); $buttons[] = array('id' => 'add_new', 'type' => MS_Helper_Html::TYPE_HTML_LINK, 'url' => MS_Controller_Plugin::get_admin_url('billing', array('action' => MS_Controller_Billing::ACTION_EDIT, 'invoice_id' => 0)), 'value' => __('Create new Invoice', 'membership2'), 'class' => 'button'); $buttons[] = array('type' => MS_Helper_Html::TYPE_HTML_LINK, 'url' => MS_Controller_Plugin::get_admin_url('billing', array('show' => 'logs')), 'value' => __('Show Transaction Logs', 'membership2'), 'class' => 'button'); if (!empty($_GET['gateway_id'])) { $gateway = MS_Model_Gateway::factory($_GET['gateway_id']); if ($gateway->name) { $title .= ' - ' . $gateway->name; } } break; } if ('matching' != $module) { if (MS_Model_Import::can_match()) { $btn_label = __('Setup automatic matching', 'membership2'); $btn_class = 'button'; } else { $btn_label = '(' . __('Setup automatic matching', 'membership2') . ')'; $btn_class = 'button button-link'; } $buttons[] = array('type' => MS_Helper_Html::TYPE_HTML_LINK, 'url' => MS_Controller_Plugin::get_admin_url('billing', array('show' => 'matching')), 'value' => $btn_label, 'class' => $btn_class); } // Default list view part - dislay prepared values from above. ob_start(); ?> <div class="wrap ms-wrap ms-billing"> <?php MS_Helper_Html::settings_header(array('title' => $title, 'title_icon_class' => 'wpmui-fa wpmui-fa-credit-card')); ?> <div> <?php foreach ($buttons as $button) { MS_Helper_Html::html_element($button); } ?> </div> <?php $listview->views(); $listview->search_box(__('User', 'membership2'), 'search'); ?> <form action="" method="post"> <?php $listview->display(); ?> </form> </div> <?php $html = ob_get_clean(); return apply_filters('ms_view_billing_list', $html, $this); }
/** * 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; } }
/** * Retries to process a single error-state transaction. * * Expected output: * OK * ERR * * @since 1.0.1.2 */ public function ajax_action_retry() { $res = 'ERR'; if (!$this->is_admin_user()) { return; } $fields_retry = array('id'); // Save details of a single invoice. if ($this->verify_nonce() && self::validate_required($fields_retry)) { $log_id = intval($_POST['id']); if (MS_Model_Import::retry_to_process($log_id)) { $res = 'OK'; } $log = MS_Factory::load('MS_Model_Transactionlog', $log_id); $res .= ':' . $log->description; } echo $res; exit; }
/** * Custom display function to hide item table for invalid filter options. * * @since 1.0.1.2 */ public function display() { if (MS_Model_Import::can_match($this->source_id, $this->source)) { parent::display(); } }