/**
  * 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();
     }
 }