/** * Tests the Stripe Subscription gateway * @test */ function stripeplan_subscription() { $gateway = MS_Model_Gateway::factory(MS_Gateway_Stripeplan::ID); $user_id = TData::id('user', 'editor'); $membership_id = TData::id('membership', 'recurring'); $subscription = TData::subscribe($user_id, $membership_id); $controller = MS_Factory::load('MS_Controller_Gateway'); $gateway->update_stripe_data(); $data = array('card' => array('number' => '4242424242424242', 'exp_month' => 12, 'exp_year' => date('Y') + 1, 'cvc' => '314')); $res = M2_Stripe_Token::create($data); $token = $res->id; $form_data = array('_wpnonce' => wp_create_nonce($gateway->id . '_' . $subscription->id), 'gateway' => $gateway->id, 'ms_relationship_id' => $subscription->id, 'step' => 'process_purchase', 'stripeToken' => $token, 'stripeTokenType' => 'card', 'stripeEmail' => '*****@*****.**'); $_POST = $form_data; $_REQUEST = $_POST; // Right now the subscription must have status PENDING $this->assertEquals(MS_Model_Relationship::STATUS_PENDING, $subscription->status); /* * This function processes the purchase and will set the subscription * to active. */ $controller->process_purchase(); // Check the subscription status. $this->assertEquals(MS_Model_Relationship::STATUS_ACTIVE, $subscription->status); $this->assertEquals(1, count($subscription->payments)); // Modify the expiration date to trigger another payment. $today = date('Y-m-d'); $subscription->expire_date = $today; $this->assertEquals($today, $subscription->expire_date); $this->assertEquals(0, $subscription->get_remaining_period()); // Trigger next payment and validate it. $subscription->check_membership_status(); $this->assertEquals(2, count($subscription->payments)); // Modify the expiration date to trigger another payment. $subscription->expire_date = $today; $this->assertEquals($today, $subscription->expire_date); $this->assertEquals(0, $subscription->get_remaining_period()); // Trigger next payment and validate it. // THIS TIME NO PAYMENT SHOULD BE MADE because paycycle_repetitions = 2! $subscription->check_membership_status(); $this->assertEquals(2, count($subscription->payments)); // Also the subscription should be cancelled at stripe now. $customer_id = $subscription->get_member()->get_gateway_profile(MS_Gateway_Stripe_Api::ID, 'customer_id'); $customer = M2_Stripe_Customer::retrieve($customer_id); $invoice = $subscription->get_previous_invoice(); $stripe_sub_id = $invoice->external_id; $stripe_sub = $customer->subscriptions->retrieve($stripe_sub_id); $this->assertEquals('active', $stripe_sub->status); $this->assertTrue($stripe_sub->cancel_at_period_end); // Clean up. $customer->delete(); }
/** * Checks if a specific payment gateway is allowed for the current * membership. * * @since 1.0.0 * @param string $gateway_id The payment gateway ID. * @return bool */ public function can_use_gateway($gateway_id) { $result = true; $this->disabled_gateways = lib3()->array->get($this->disabled_gateways); if (isset($this->disabled_gateways[$gateway_id])) { $state = $this->disabled_gateways[$gateway_id]; $result = !lib3()->is_true($state); } if ($result) { $gateway = MS_Model_Gateway::factory($gateway_id); $result = $gateway->payment_type_supported($this); } $result = apply_filters('ms_model_membership_can_use_gateway', $result, $gateway_id, $this); return $result; }
/** * Renders Authorize.net CIM profiles. * * @since 1.0.0 * * @access protected */ protected function render_cim_profiles($fields) { // if profile is empty, then return if (empty($this->data['cim_profiles'])) { return; } $gateway = MS_Model_Gateway::factory(MS_Gateway_Authorize::ID); $cim_profiles = $this->data['cim_profiles']; // if we have one record in profile, then wrap it into array to make it // compatible with case when we have more then one payment methods added if (isset($cim_profiles['billTo'])) { $cim_profiles = array($cim_profiles); } $first_key = null; foreach ($cim_profiles as $index => $profile) { if (is_array($profile) && !empty($profile['customerPaymentProfileId'])) { $options[$profile['customerPaymentProfileId']] = esc_html(sprintf("%s %s's - **** **** **** %s ", $profile['billTo']['firstName'], $profile['billTo']['lastName'], str_replace('XXXX', '', $profile['payment']['creditCard']['cardNumber']))); if (!$first_key) { $first_key = $profile['customerPaymentProfileId']; } } } $options[0] = __('Enter a new credit card', 'membership2'); $cim = array('id' => 'profile', 'type' => MS_Helper_Html::INPUT_TYPE_RADIO, 'field_options' => $options, 'value' => $first_key); if ($this->data['cim_payment_profile_id']) { $cim['value'] = $this->data['cim_payment_profile_id']; } $card_cvc = array('id' => 'card_code', 'title' => __('Enter the credit cards CVC code to verify the payment', 'membership2'), 'type' => MS_Helper_Html::INPUT_TYPE_TEXT, 'placeholder' => 'CVC', 'maxlength' => 4); ?> <form id="ms-authorize-extra-form" method="post" class="ms-form"> <?php foreach ($fields['hidden'] as $field) { ?> <?php MS_Helper_Html::html_element($field); ?> <?php } ?> <div id="ms-authorize-cim-profiles-wrapper" class="authorize-form-block"> <table> <tr> <td class="ms-title-row"><?php _e('Stored Credit Cards', 'membership2'); ?> </td> </tr> <tr> <td class="ms-col-cim_profiles"> <?php MS_Helper_Html::html_element($cim); ?> </td> </tr> <?php if (lib3()->is_true($gateway->secure_cc)) { ?> <tr class="ms-row-card_cvc"> <td> <?php MS_Helper_Html::html_element($card_cvc); ?> </td> </tr> <?php } ?> <tr class="ms-row-submit"> <td class="ms-col-submit"> <?php MS_Helper_Html::html_element($fields['submit']); ?> </td> </tr> </table> </div> </form> <?php }
/** * Get related gateway model. * * @since 1.0.0 * @api * * @return MS_Model_Gateway */ public function get_gateway() { $gateway = MS_Model_Gateway::factory($this->gateway_id); return apply_filters('ms_model_relationship_get_gateway', $gateway); }
/** * Membership invoice shortcode callback function. * * @since 1.0.0 * * @param mixed[] $atts Shortcode attributes. */ public function membership_invoice($atts) { MS_Helper_Shortcode::did_shortcode(MS_Helper_Shortcode::SCODE_MS_INVOICE); $data = apply_filters('ms_controller_shortcode_invoice_atts', shortcode_atts(array('post_id' => 0, 'id' => 0, 'pay_button' => 1), $atts, MS_Helper_Shortcode::SCODE_MS_INVOICE)); if (!empty($data['id'])) { $data['post_id'] = $data['id']; } if (!empty($data['post_id'])) { $invoice = MS_Factory::load('MS_Model_Invoice', $data['post_id']); $subscription = MS_Factory::load('MS_Model_Relationship', $invoice->ms_relationship_id); $data['invoice'] = $invoice; $data['member'] = MS_Factory::load('MS_Model_Member', $invoice->user_id); $data['ms_relationship'] = $subscription; $data['membership'] = $subscription->get_membership(); $data['gateway'] = MS_Model_Gateway::factory($invoice->gateway_id); $view = MS_Factory::create('MS_View_Shortcode_Invoice'); $view->data = apply_filters('ms_view_shortcode_invoice_data', $data, $this); return $view->to_html(); } }
/** * Resets the database. * * @since 1.0.0 */ public static function reset() { global $wpdb; // wipe all existing data. $wpdb->query("TRUNCATE TABLE {$wpdb->users};"); $wpdb->query("TRUNCATE TABLE {$wpdb->usermeta};"); $wpdb->query("TRUNCATE TABLE {$wpdb->posts};"); $wpdb->query("TRUNCATE TABLE {$wpdb->postmeta};"); $wpdb->query("DELETE FROM {$wpdb->options} WHERE option_name LIKE '%transient_%';"); $wpdb->query("DELETE FROM {$wpdb->options} WHERE option_name LIKE 'MS_%';"); self::$ids = array('user' => array(), 'post' => array(), 'membership' => array()); // create demo users $users = array('admin' => array('role' => 'administrator'), 'editor' => array('role' => 'editor')); foreach ($users as $login => $data) { $defaults = array('user_login' => $login, 'user_pass' => $login . '-password', 'user_email' => $login . '@local.dev', 'role' => 'subscriber', 'user_nicename' => '', 'user_url' => '', 'display_name' => 'User ' . $login, 'nickname' => '', 'first_name' => '', 'last_name' => '', 'description' => '', 'user_registered' => ''); $data = shortcode_atts($defaults, $data); $id = wp_insert_user($data); if (!empty($data['meta'])) { foreach ($data['meta'] as $key => $val) { $val = maybe_serialize($val); update_user_meta($id, $key, $val); } } self::$ids['user'][$login] = $id; } // create demo posts $posts = array('sample-post' => array('post_content' => 'Just a very simple sample post...'), 'sample-page' => array('post_type' => 'page', 'post_content' => 'Just a very simple sample page...')); foreach ($posts as $slug => $data) { $defaults = array('post_type' => 'post', 'post_author' => self::id('user', 'admin'), 'post_title' => $slug, 'post_name' => $slug); $data = shortcode_atts($defaults, $data); $id = wp_insert_post($data); if (!empty($data['meta'])) { foreach ($data['meta'] as $key => $val) { $val = maybe_serialize($val); update_post_meta($id, $key, $val); } } self::$ids['post'][$slug] = $id; } // create demo memberships $memberships = array('simple' => array('name' => 'Simple Membership', 'type' => MS_Model_Membership::TYPE_STANDARD, 'payment_type' => MS_Model_Membership::PAYMENT_TYPE_PERMANENT, 'price' => 29, 'rule_values' => array()), 'simple-free' => array('name' => 'Simple Membership', 'type' => MS_Model_Membership::TYPE_STANDARD, 'payment_type' => MS_Model_Membership::PAYMENT_TYPE_PERMANENT, 'is_free' => true, 'price' => 0, 'rule_values' => array()), 'simple-trial' => array('name' => 'Simple Membership with Trial', 'type' => MS_Model_Membership::TYPE_STANDARD, 'payment_type' => MS_Model_Membership::PAYMENT_TYPE_PERMANENT, 'price' => 29, 'rule_values' => array(), 'trial_period_enabled' => true, 'trial_period' => array('period_unit' => 14, 'period_type' => 'days')), 'limited' => array('name' => 'Limited Membership', 'type' => MS_Model_Membership::TYPE_STANDARD, 'payment_type' => MS_Model_Membership::PAYMENT_TYPE_FINITE, 'price' => 19, 'rule_values' => array(), 'period' => array('period_unit' => 28, 'period_type' => 'days')), 'limited-trial' => array('name' => 'Limited Membership with Trial', 'type' => MS_Model_Membership::TYPE_STANDARD, 'payment_type' => MS_Model_Membership::PAYMENT_TYPE_FINITE, 'price' => 19, 'rule_values' => array(), 'period' => array('period_unit' => 28, 'period_type' => 'days'), 'trial_period_enabled' => true, 'trial_period' => array('period_unit' => 14, 'period_type' => 'days')), 'daterange-trial' => array('name' => 'Date-Range Membership with Trial', 'type' => MS_Model_Membership::TYPE_STANDARD, 'payment_type' => MS_Model_Membership::PAYMENT_TYPE_DATE_RANGE, 'price' => 39, 'rule_values' => array(), 'period_date_start' => date('Y-m-d', time() + self::ONE_DAY), 'period_date_end' => date('Y-m-d', time() + 10 * self::ONE_DAY), 'trial_period_enabled' => true, 'trial_period' => array('period_unit' => 14, 'period_type' => 'days')), 'free-limited' => array('name' => 'Free Limited Membership', 'type' => MS_Model_Membership::TYPE_STANDARD, 'payment_type' => MS_Model_Membership::PAYMENT_TYPE_FINITE, 'is_free' => true, 'price' => 0, 'rule_values' => array(), 'period' => array('period_unit' => 28, 'period_type' => 'days')), 'recurring' => array('name' => 'Unit-Test Recurring', 'type' => MS_Model_Membership::TYPE_STANDARD, 'payment_type' => MS_Model_Membership::PAYMENT_TYPE_RECURRING, 'is_free' => false, 'price' => 4, 'rule_values' => array(), 'pay_cycle_period' => array('period_unit' => 7, 'period_type' => 'days'), 'pay_cycle_repetitions' => 2)); foreach ($memberships as $key => $data) { $item = new MS_Model_Membership(); foreach ($data as $prop => $val) { if (!property_exists($item, $prop)) { continue; } $item->{$prop} = $val; } $item->save(); $id = $item->id; self::$ids['membership'][$key] = $id; } // Prepare Payment Gateways. $gateway = MS_Model_Gateway::factory(MS_Gateway_Stripe::ID); $gateway->mode = MS_Gateway::MODE_SANDBOX; $gateway->active = true; $gateway->test_secret_key = 'sk_test_MSKvYHhIm3kKNr4tshnZHIEk'; $gateway->test_publishable_key = 'pk_test_h8fk0CAW287ToA3o6aeehThB'; $gateway->save(); $gateway = MS_Model_Gateway::factory(MS_Gateway_Stripeplan::ID); $gateway->mode = MS_Gateway::MODE_SANDBOX; $gateway->active = true; $gateway->test_secret_key = 'sk_test_MSKvYHhIm3kKNr4tshnZHIEk'; $gateway->test_publishable_key = 'pk_test_h8fk0CAW287ToA3o6aeehThB'; $gateway->save(); // Clear the plugin Factory-Cache MS_Factory::_reset(); }
/** * Handle update credit card information in gateway. * * Used to change credit card info in account's page. * * Related action hooks: * - template_redirect * * @since 1.0.0 */ public function update_card() { if (!empty($_POST['gateway'])) { $gateway = MS_Model_Gateway::factory($_POST['gateway']); $member = MS_Model_Member::get_current_member(); switch ($gateway->id) { case MS_Gateway_Stripe::ID: if (!empty($_POST['stripeToken']) && $this->verify_nonce()) { lib2()->array->strip_slashes($_POST, 'stripeToken'); $gateway->add_card($member, $_POST['stripeToken']); if (!empty($_POST['ms_relationship_id'])) { $ms_relationship = MS_Factory::load('MS_Model_Relationship', $_POST['ms_relationship_id']); MS_Model_Event::save_event(MS_Model_Event::TYPE_UPDATED_INFO, $ms_relationship); } wp_safe_redirect(esc_url_raw(add_query_arg(array('msg' => 1)))); exit; } break; case MS_Gateway_Authorize::ID: if ($this->verify_nonce()) { do_action('ms_controller_frontend_signup_gateway_form', $this); } elseif (!empty($_POST['ms_relationship_id']) && $this->verify_nonce($_POST['gateway'] . '_' . $_POST['ms_relationship_id'])) { $gateway->update_cim_profile($member); $gateway->save_card_info($member); if (!empty($_POST['ms_relationship_id'])) { $ms_relationship = MS_Factory::load('MS_Model_Relationship', $_POST['ms_relationship_id']); MS_Model_Event::save_event(MS_Model_Event::TYPE_UPDATED_INFO, $ms_relationship); } wp_safe_redirect(esc_url_raw(add_query_arg(array('msg' => 1)))); exit; } break; default: break; } } do_action('ms_controller_gateway_update_card', $this); }
/** * Tries to process a single transaction again. * * This function is only useful when the transaction matching was added * before callig it again. * * @since 1.0.1.2 * @param int $transaction_id The ID of the transaction log item. * @return bool True means that the transaction was processed. */ public static function retry_to_process($transaction_id) { $res = false; $log = MS_Factory::load('MS_Model_Transactionlog', $transaction_id); if (empty($log) || $log->id != $transaction_id) { // Could not find the requested transaction log item. return $res; } if ('ok' == $log->state) { // The transaction was already processed (automatically or manual). return $res; } $post_data = $log->post; if (empty($post_data) || !is_array($post_data)) { // We do not have POST data available for the transaction. // Re-Processing is not possible. return $res; } $orig_post = $_POST; $orig_req = $_REQUEST; // Set up the PHP environment to process the transaction again. $gateway = MS_Model_Gateway::factory($log->gateway_id); $_POST = $post_data; $_REQUEST = $post_data; switch ($log->method) { case 'request': // Intentionally not implemented: // Request payment needs a subscription to work. break; case 'process': // Intentionally not implemented: // Request payment needs a subscription to work. break; case 'handle': $log = $gateway->handle_return($log); break; } if ('ok' == $log->state) { $res = true; } $_POST = $orig_post; $_REQUEST = $orig_req; return $res; }
/** * Create view output. * * @since 1.0.0 * * @return string */ public function to_html() { $this->check_simulation(); $buttons = array(); // Count invalid transactions. $args = array('state' => 'err'); $error_count = MS_Model_Transactionlog::get_item_count($args); if ($error_count && (empty($_GET['state']) || 'err' != $_GET['state'])) { 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.', MS_TEXT_DOMAIN); } 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.', MS_TEXT_DOMAIN); } $review_url = MS_Controller_Plugin::get_admin_url('billing', array('show' => 'logs', 'state' => 'err')); lib2()->ui->admin_message(sprintf($message, $error_count, '<a href="' . $review_url . '">', '</a>'), 'err'); } if (isset($_GET['show']) && 'logs' == $_GET['show']) { $title = __('Transaction Logs', MS_TEXT_DOMAIN); $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', MS_TEXT_DOMAIN), 'class' => 'button'); } else { $title = __('Billing', MS_TEXT_DOMAIN); $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', MS_TEXT_DOMAIN), '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', MS_TEXT_DOMAIN), 'class' => 'button'); if (!empty($_GET['gateway_id'])) { $gateway = MS_Model_Gateway::factory($_GET['gateway_id']); if ($gateway->name) { $title .= ' - ' . $gateway->name; } } } 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', MS_TEXT_DOMAIN), 'search'); ?> <form action="" method="post"> <?php $listview->display(); ?> </form> </div> <?php $html = ob_get_clean(); return apply_filters('ms_view_billing_list', $html, $this); }
/** * 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); }