payment_method_is_overridden( 'check_status' ) ) { $this->setup_cron(); } } /** * Runs when the payment add-on is initialized. * * @since Unknown * @access public * * @uses GFFeedAddOn::init() * @uses GFPaymentAddOn::confirmation() * @uses GFPaymentAddOn::maybe_validate() * @uses GFPaymentAddOn::entry_post_save() * * @return void */ public function init() { parent::init(); add_filter( 'gform_confirmation', array( $this, 'confirmation' ), 20, 4 ); add_filter( 'gform_validation', array( $this, 'maybe_validate' ), 20 ); add_filter( 'gform_entry_post_save', array( $this, 'entry_post_save' ), 10, 2 ); if ( $this->_requires_credit_card ) { add_filter( 'gform_register_init_scripts', array( $this, 'register_creditcard_token_script' ), 10, 3 ); add_filter( 'gform_field_content', array( $this, 'add_creditcard_token_input' ), 10, 5 ); add_filter( 'gform_form_args', array( $this, 'force_ajax_for_creditcard_tokens' ), 10, 1 ); } add_filter( 'gform_is_delayed_pre_process_feed', array( $this, 'maybe_delay_feed_processing' ), 20, 4 ); } /** * Runs only when the payment add-on is initialized in the admin. * * @since Unknown * @access public * * @uses GFFeedAddOn::init_admin() * @uses GFPaymentAddOn::$_requires_credit_card * @uses GFPaymentAddOn::supported_currencies() * @uses GFPaymentAddOn::entry_deleted() * @uses GFPaymentAddOn::entry_info() * * @return void */ public function init_admin() { parent::init_admin(); if ( $this->_requires_credit_card ) { // Enable the credit card field. add_filter( 'gform_enable_credit_card_field', '__return_true' ); } add_filter( 'gform_currencies', array( $this, 'supported_currencies' ) ); add_filter( 'gform_delete_lead', array( $this, 'entry_deleted' ) ); add_action( 'gform_before_delete_field', array( $this, 'before_delete_field' ), 10, 2 ); if ( rgget( 'page' ) == 'gf_entries' ) { add_action( 'gform_payment_details', array( $this, 'entry_info' ), 10, 2 ); } } /** * Runs only when AJAX actions are being performed. * * @since Unknown * @access public * * @uses GFFeedAddOn::init_ajax() * @uses GFPaymentAddOn::ajax_cancel_subscription() * @uses GFPaymentAddOn::before_delete_field() * * @return void */ public function init_ajax() { parent::init_ajax(); add_action( 'wp_ajax_gaddon_cancel_subscription', array( $this, 'ajax_cancel_subscription' ) ); } /** * Runs the setup of the payment add-on. * * @since Unknown * @access public * * @uses GFFeedAddOn::setup() * @uses GFPaymentAddOn::upgrade_payment() * @uses GFAddOn::$_slug * @uses GFPaymentAddOn::$_payment_version * * @return void */ public function setup() { parent::setup(); $installed_version = get_option( 'gravityformsaddon_payment_version' ); $installed_addons = get_option( 'gravityformsaddon_payment_addons' ); if ( ! is_array( $installed_addons ) ) { $installed_addons = array(); } if ( $installed_version != $this->_payment_version ) { $this->upgrade_payment( $installed_version ); $installed_addons = array( $this->_slug ); update_option( 'gravityformsaddon_payment_addons', $installed_addons ); } elseif ( ! in_array( $this->_slug, $installed_addons ) ) { $this->upgrade_payment( $installed_version ); $installed_addons[] = $this->_slug; update_option( 'gravityformsaddon_payment_addons', $installed_addons ); } update_option( 'gravityformsaddon_payment_version', $this->_payment_version ); } /** * Upgrades the payment add-on framework database tables. * * Not intended to be used. * * @since Unknown * @access private * * @uses GFFormsModel::dbDelta() * @uses GFPaymentAddOn::$_supports_callbacks * @uses GFForms::drop_index() * * @global $wpdb * @param null $previous_versions Not used. * * @return void */ private function upgrade_payment( $previous_versions ) { global $wpdb; $charset_collate = GFFormsModel::get_db_charset(); $sql = "CREATE TABLE {$wpdb->prefix}gf_addon_payment_transaction ( id int(10) unsigned not null auto_increment, lead_id int(10) unsigned not null, transaction_type varchar(30) not null, transaction_id varchar(50), subscription_id varchar(50), is_recurring tinyint(1) not null default 0, amount decimal(19,2), date_created datetime, PRIMARY KEY (id), KEY lead_id (lead_id), KEY transaction_type (transaction_type), KEY type_lead (lead_id,transaction_type) ) $charset_collate;"; gf_upgrade()->dbDelta( $sql ); if ( $this->_supports_callbacks ) { $sql = "CREATE TABLE {$wpdb->prefix}gf_addon_payment_callback ( id int(10) unsigned not null auto_increment, lead_id int(10) unsigned not null, addon_slug varchar(250) not null, callback_id varchar(250), date_created datetime, PRIMARY KEY (id), KEY addon_slug_callback_id (addon_slug(50),callback_id(100)) ) $charset_collate;"; gf_upgrade()->dbDelta( $sql ); // Dropping legacy index. gf_upgrade()->drop_index( "{$wpdb->prefix}gf_addon_payment_callback", 'slug_callback_id' ); } } /** * Gets called when Gravity Forms upgrade process is completed. This function is intended to be used internally, override the upgrade() function to execute database update scripts. * @param $db_version - Current Gravity Forms database version * @param $previous_db_version - Previous Gravity Forms database version * @param $force_upgrade - True if this is a request to force an upgrade. False if this is a standard upgrade (due to version change) */ public function post_gravityforms_upgrade( $db_version, $previous_db_version, $force_upgrade ){ // Forcing Upgrade if( $force_upgrade ){ $installed_version = get_option( 'gravityformsaddon_payment_version' ); $this->upgrade_payment( $installed_version ); update_option( 'gravityformsaddon_payment_version', $this->_payment_version ); } parent::post_gravityforms_upgrade( $db_version, $previous_db_version, $force_upgrade ); } //--------- Delayed Feeds ------ /** * Determines if feed processing is delayed by the payment feed configuration. * * @since 2.4.13 * * @param bool $is_delayed Is feed processing delayed? * @param array $form The form currently being processed. * @param array $entry The entry currently being processed. * @param string $slug The Add-On slug e.g. gravityformsmailchimp * * @return bool */ public function maybe_delay_feed_processing( $is_delayed, $form, $entry, $slug ) { if ( $is_delayed || ! $this->is_payment_gateway( $entry['id'] ) ) { return $is_delayed; } $payment_feed = $this->current_feed; return (bool) rgars( $payment_feed, 'meta/delay_' . $slug ); } /** * Triggers processing of delayed feeds for other add-ons. * * @since 2.4.13 * * @param string $transaction_id The transaction or subscription ID. * @param array $payment_feed The payment feed which originated the transaction. * @param array $entry The entry currently being processed. * @param array $form The form currently being processed. */ public function trigger_payment_delayed_feeds( $transaction_id, $payment_feed, $entry, $form ) { if ( has_filter( 'gform_trigger_payment_delayed_feeds' ) ) { $this->log_debug( __METHOD__ . '(): Executing functions hooked to gform_trigger_payment_delayed_feeds.' ); /** * Used in GFFeedAddOn to trigger processing of feeds delayed until payment is completed. * * @since 2.4.13 * * @param string $transaction_id The transaction or subscription ID. * @param array $payment_feed The payment feed which originated the transaction. * @param array $entry The entry currently being processed. * @param array $form The form currently being processed. */ do_action( 'gform_trigger_payment_delayed_feeds', $transaction_id, $payment_feed, $entry, $form ); } } /** * Override to specify where the "Post Payment Action" setting should appear on the payment add-on feed. * * @since 2.4.13 * * @param string $feed_slug The feed add-on slug. * * @return array */ public function get_post_payment_actions_config( $feed_slug ) { // We specify PayPal here for backwards capability, in case the PayPal add-on < 3.3 // hasn't implemented get_post_payment_actions_config(). if ( $this->get_slug() === 'gravityformspaypal' ) { $config = array( 'position' => 'after', 'setting' => 'options', ); } else { $config = array(); } return $config; } //--------- Submission Process ------ /** * Handles post-submission confirmations. * * @since Unknown * @access public * * @uses GFPaymentAddOn::$redirect_url * * @param array $confirmation The confirmation details. * @param array $form The Form Object that the confirmation is being run for. * @param array $entry The Entry Object associated with the submission. * @param bool $ajax If the submission was done using AJAX. * * @return array The confirmation details. */ public function confirmation( $confirmation, $form, $entry, $ajax ) { if ( empty( $this->redirect_url ) ) { return $confirmation; } $confirmation = array( 'redirect' => $this->redirect_url ); return $confirmation; } /** * Override this function to specify a URL to the third party payment processor. * * Useful when developing a payment gateway that processes the payment outside of the website (i.e. PayPal Standard). * * @since Unknown * @access public * * @used-by GFPaymentAddOn::entry_post_save() * * @param array $feed Active payment feed containing all the configuration data. * @param array $submission_data Contains form field data submitted by the user as well as payment information (i.e. payment amount, setup fee, line items, etc...). * @param array $form Current form array containing all form settings. * @param array $entry Current entry array containing entry information (i.e data submitted by users). * * @return void|string Return a full URL (including http:// or https://) to the payment processor. */ public function redirect_url( $feed, $submission_data, $form, $entry ) { } /** * Check if the rest of the form has passed validation, is the last page, and that the honeypot field has not been completed. * * @since Unknown * @access public * * @used-by GFPaymentAddOn::init() * @uses GFFormDisplay::is_last_page() * @uses GFFormDisplay::get_max_field_id() * @uses GFPaymentAddOn::validation() * * @param array $validation_result Contains the validation result, the Form Object, and the failed validation page number. * * @return array $validation_result */ public function maybe_validate( $validation_result ) { $form = $validation_result['form']; $is_last_page = GFFormDisplay::is_last_page( $form ); $failed_honeypot = false; if ( $is_last_page && rgar( $form, 'enableHoneypot' ) ) { $honeypot_id = GFFormDisplay::get_max_field_id( $form ) + 1; $failed_honeypot = ! rgempty( "input_{$honeypot_id}" ); } // Validation called by partial entries feature via the heartbeat API. $is_heartbeat = rgpost('action') == 'heartbeat'; if ( ! $validation_result['is_valid'] || ! $is_last_page || $failed_honeypot || $is_heartbeat ) { return $validation_result; } return $this->validation( $validation_result ); } /** * Handles the validation and processing of payments. * * @since Unknown * @access public * * @uses GFPaymentAddOn::get_payment_feed * @uses GFPaymentAddOn::get_submission_data * @uses GFPaymentAddOn::$is_payment_gateway * @uses GFPaymentAddOn::$current_feed * @uses GFPaymentAddOn::$current_submission_data * @uses GFPaymentAddOn::payment_method_is_overridden * @uses GFPaymentAddOn::authorize * @uses GFPaymentAddOn::subscribe * @uses GFPaymentAddOn::get_validation_result * @uses GFPaymentAddOn::$authorization * @uses GFFeedAddOn::$_single_submission_feed * @uses GFFormsModel::create_lead * @uses GFAddOn::log_debug * @uses GFFormDisplay::set_current_page * * @param array $validation_result The validation details to use. * * @return array The validation details after completion. */ public function validation( $validation_result ) { if ( ! $validation_result['is_valid'] ) { return $validation_result; } $form = $validation_result['form']; $entry = GFFormsModel::create_lead( $form ); $feed = $this->get_payment_feed( $entry, $form ); if ( ! $feed ) { return $validation_result; } global $gf_payment_gateway; if ( $gf_payment_gateway && $gf_payment_gateway !== $this->get_slug() ) { $this->log_debug( __METHOD__ . '() Aborting. Submission already processed by ' . $gf_payment_gateway ); return $validation_result; } $submission_data = $this->get_submission_data( $feed, $form, $entry ); // Do not process payment if payment amount is 0. if ( floatval( $submission_data['payment_amount'] ) <= 0 ) { $this->log_debug( __METHOD__ . '(): Payment amount is zero or less. Not sending to payment gateway.' ); return $validation_result; } if ( GFCommon::is_spam_entry( $entry, $form ) ) { $this->log_debug( __METHOD__ . '() Aborting. Submission flagged as spam.' ); return $validation_result; } $gf_payment_gateway = $this->get_slug(); $this->is_payment_gateway = true; $this->current_feed = $this->_single_submission_feed = $feed; $this->current_submission_data = $submission_data; $performed_authorization = false; $is_subscription = $feed['meta']['transactionType'] == 'subscription'; if ( $this->payment_method_is_overridden( 'authorize' ) && ! $is_subscription ) { //Running an authorization only transaction if function is implemented and this is a single payment $this->authorization = $this->authorize( $feed, $submission_data, $form, $entry ); $performed_authorization = true; } elseif ( $this->payment_method_is_overridden( 'subscribe' ) && $is_subscription ) { $subscription = $this->subscribe( $feed, $submission_data, $form, $entry ); $this->authorization['is_authorized'] = rgar($subscription,'is_success'); $this->authorization['error_message'] = rgar( $subscription, 'error_message' ); $this->authorization['subscription'] = $subscription; $performed_authorization = true; } if ( $performed_authorization ) { $this->log_debug( __METHOD__ . "(): Authorization result for form #{$form['id']} submission => " . print_r( $this->authorization, 1 ) ); } if ( $performed_authorization && ! rgar( $this->authorization, 'is_authorized' ) ) { $validation_result = $this->get_validation_result( $validation_result, $this->authorization ); //Setting up current page to point to the credit card page since that will be the highlighted field GFFormDisplay::set_current_page( $validation_result['form']['id'], $validation_result['credit_card_page'] ); } return $validation_result; } /** * Override this method to add integration code to the payment processor in order to authorize a credit card with or * without capturing payment. * * This method is executed during the form validation process and allows the form submission process to fail with a * validation error if there is anything wrong with the payment/authorization. This method is only supported by * single payments. For subscriptions or recurring payments, use the GFPaymentAddOn::subscribe() method. * * @since Unknown * @access public * * @used-by GFPaymentAddOn::validation() * * @param array $feed Current configured payment feed. * @param array $submission_data Contains form field data submitted by the user as well as payment information * (i.e. payment amount, setup fee, line items, etc...). * @param array $form The Form Object. * @param array $entry The Entry Object. NOTE: the entry hasn't been saved to the database at this point, * so this $entry object does not have the 'ID' property and is only a memory * representation of the entry. * * @return array { * Return an $authorization array. * * @type bool $is_authorized True if the payment is authorized. Otherwise, false. * @type string $error_message The error message, if present. * @type string $transaction_id The transaction ID. * @type array $captured_payment { * If payment is captured, an additional array is created. * * @type bool $is_success If the payment capture is successful. * @type string $error_message The error message, if any. * @type string $transaction_id The transaction ID of the captured payment. * @type int $amount The amount of the captured payment, if successful. * } * } */ public function authorize( $feed, $submission_data, $form, $entry ) { } /** * Override this method to capture a single payment that has been authorized via the authorize() method. * * Use only with single payments. For subscriptions, use subscribe() instead. * * @since Unknown * @access public * * @used-by GFPaymentAddOn::entry_post_save() * * @param array $authorization Contains the result of the authorize() function. * @param array $feed Current configured payment feed. * @param array $submission_data Contains form field data submitted by the user as well as payment information. * (i.e. payment amount, setup fee, line items, etc...). * @param array $form Current form array containing all form settings. * @param array $entry Current entry array containing entry information (i.e data submitted by users). * * @return array { * Return an array with the information about the captured payment in the following format: * * @type bool $is_success If the payment capture is successful. * @type string $error_message The error message, if any. * @type string $transaction_id The transaction ID of the captured payment. * @type int $amount The amount of the captured payment, if successful. * @type string $payment_method The card issuer. * } */ public function capture( $authorization, $feed, $submission_data, $form, $entry ) { } /** * Override this method to add integration code to the payment processor in order to create a subscription. * * This method is executed during the form validation process and allows the form submission process to fail with a * validation error if there is anything wrong when creating the subscription. * * @since Unknown * @access public * * @used-by GFPaymentAddOn::validation() * * @param array $feed Current configured payment feed. * @param array $submission_data Contains form field data submitted by the user as well as payment information * (i.e. payment amount, setup fee, line items, etc...). * @param array $form Current form array containing all form settings. * @param array $entry Current entry array containing entry information (i.e data submitted by users). * NOTE: the entry hasn't been saved to the database at this point, so this $entry * object does not have the 'ID' property and is only a memory representation of the entry. * * @return array { * Return an $subscription array in the following format: * * @type bool $is_success If the subscription is successful. * @type string $error_message The error message, if applicable. * @type string $subscription_id The subscription ID. * @type int $amount The subscription amount. * @type array $captured_payment { * If payment is captured, an additional array is created. * * @type bool $is_success If the payment capture is successful. * @type string $error_message The error message, if any. * @type string $transaction_id The transaction ID of the captured payment. * @type int $amount The amount of the captured payment, if successful. * } * * To implement an initial/setup fee for gateways that don't support setup fees as part of subscriptions, manually * capture the funds for the setup fee as a separate transaction and send that payment information in the * following 'captured_payment' array: * * 'captured_payment' => [ * 'name' => 'Setup Fee', * 'is_success' => true|false, * 'error_message' => 'error message', * 'transaction_id' => 'xxx', * 'amount' => 20 * ] */ public function subscribe( $feed, $submission_data, $form, $entry ) { } /** * Override this method to add integration code to the payment processor in order to cancel a subscription. * * This method is executed when a subscription is canceled from the Payment Gateway (i.e. Stripe or PayPal). * * @since Unknown * @access public * * @used-by GFPaymentAddOn::ajax_cancel_subscription() * * @param array $entry Current entry array containing entry information (i.e data submitted by users). * @param array $feed Current configured payment feed. * * @return bool Returns true if the subscription was cancelled successfully and false otherwise. * */ public function cancel( $entry, $feed ) { return false; } /** * Gets the payment validation result. * * @since Unknown * @access public * * @used-by GFPaymentAddOn::validation() * * @param array $validation_result Contains the form validation results. * @param array $authorization_result Contains the form authorization results. * * @return array The validation result for the credit card field. */ public function get_validation_result( $validation_result, $authorization_result ) { $credit_card_page = 0; foreach ( $validation_result['form']['fields'] as &$field ) { if ( $field->type == 'creditcard' ) { $field->failed_validation = true; $field->validation_message = $authorization_result['error_message']; $credit_card_page = $field->pageNumber; break; } } $validation_result['credit_card_page'] = $credit_card_page; $validation_result['is_valid'] = false; return $validation_result; } /** * Sets the processed feed meta. * * @since 2.4.13 Overrode to prevent processed feed meta being set when a different add-on processed the submission. * * @param array $entry The Entry Object currently being processed. * @param array $form The Form Object currently being processed. * * @return array */ public function maybe_process_feed( $entry, $form ) { global $gf_payment_gateway; if ( $gf_payment_gateway && $gf_payment_gateway !== $this->get_slug() ) { return $entry; } return parent::maybe_process_feed( $entry, $form ); } /** * Handles additional processing after an entry is saved. * * @since Unknown * @access public * * @used-by GFPaymentAddOn::init() * @uses GFPaymentAddOn::$is_payment_gateway * @uses GFPaymentAddOn::$current_feed * @uses GFPaymentAddOn::$authorization * @uses GFPaymentAddOn::process_subscription() * @uses GFPaymentAddOn::payment_method_is_overridden() * @uses GFPaymentAddOn::process_capture() * @uses GFPaymentAddOn::redirect_url() * * @param array $entry The Entry Object. * @param array $form The Form Object. * * @return array The Entry Object. */ public function entry_post_save( $entry, $form ) { if ( ! $this->is_payment_gateway ) { return $entry; } // Saving which gateway was used to process this entry. gform_update_meta( $entry['id'], 'payment_gateway', $this->_slug ); $feed = $this->current_feed; if ( ! empty( $this->authorization ) ) { // If an authorization was done, capture it. if ( $feed['meta']['transactionType'] == 'subscription' ) { $entry = $this->process_subscription( $this->authorization, $feed, $this->current_submission_data, $form, $entry ); } else { if ( $this->payment_method_is_overridden( 'capture' ) && rgempty( 'captured_payment', $this->authorization ) ) { $this->authorization['captured_payment'] = $this->capture( $this->authorization, $feed, $this->current_submission_data, $form, $entry ); } $entry = $this->process_capture( $this->authorization, $feed, $this->current_submission_data, $form, $entry ); } } elseif ( $this->payment_method_is_overridden( 'redirect_url' ) ) { // If the url_redirect() function is overridden, call it. // Getting URL to redirect to ( saved to be used by the confirmation() function ). $this->redirect_url = $this->redirect_url( $feed, $this->current_submission_data, $form, $entry ); // Setting transaction_type to subscription or one time payment. $entry['transaction_type'] = rgars( $feed, 'meta/transactionType' ) == 'subscription' ? 2 : 1; $entry['payment_status'] = 'Processing'; } return $entry; } /** * Processed the capturing of payments. * * @since Unknown * @access public * * @used-by GFPaymentAddOn::entry_post_save() * @uses GFPaymentAddOn::complete_authorization() * @uses GFPaymentAddOn::complete_payment() * @uses GFPaymentAddOn::fail_payment() * * @param array $authorization The payment authorization details. * @param array $feed The Feed Object. * @param array $submission_data The form submission data. * @param array $form The Form Object. * @param array $entry The Entry Object. * * @return array The Entry Object. */ public function process_capture( $authorization, $feed, $submission_data, $form, $entry ) { $payment = rgar( $authorization, 'captured_payment' ); if ( empty( $payment ) && rgar( $authorization, 'is_authorized' ) ) { if ( ! rgar( $authorization, 'amount' ) ) { $authorization['amount'] = rgar( $submission_data, 'payment_amount' ); } $this->complete_authorization( $entry, $authorization ); return $entry; } $this->log_debug( __METHOD__ . "(): Updating entry #{$entry['id']} with result => " . print_r( $payment, 1 ) ); if ( $payment['is_success'] ) { $entry['is_fulfilled'] = '1'; $payment['payment_status'] = 'Paid'; $payment['payment_date'] = gmdate( 'Y-m-d H:i:s' ); $payment['type'] = 'complete_payment'; $this->complete_payment( $entry, $payment ); } else { $entry['payment_status'] = 'Failed'; $payment['type'] = 'fail_payment'; $payment['note'] = sprintf( esc_html__( 'Payment failed to be captured. Reason: %s', 'gravityforms' ), $payment['error_message'] ); $this->fail_payment( $entry, $payment ); } return $entry; } /** * Processes payment subscriptions. * * @since Unknown * @access public * * @used-by GFPaymentAddOn::entry_post_save() * @uses GFPaymentAddOn::insert_transaction() * @uses GFCommon::to_money() * @uses GFAddOn::add_note() * @uses GFPaymentAddOn::start_subscription() * @uses GFAPI::update_entry() * @uses GFPaymentAddOn::post_payment_action() * * @param array $authorization The payment authorization details. * @param array $feed The Feed Object. * @param array $submission_data The form submission data. * @param array $form The Form Object. * @param array $entry The Entry Object. * * @return array The Entry Object. */ public function process_subscription( $authorization, $feed, $submission_data, $form, $entry ) { $subscription = rgar( $authorization, 'subscription' ); if ( empty( $subscription ) ) { return $entry; } $this->log_debug( __METHOD__ . "(): Updating entry #{$entry['id']} with result => " . print_r( $subscription, 1 ) ); // If setup fee / trial is captured as part of a separate transaction. $payment = rgar( $subscription, 'captured_payment' ); $payment_name = rgempty( 'name', $payment ) ? esc_html__( 'Initial payment', 'gravityforms' ) : $payment['name']; if ( $payment && $payment['is_success'] ) { $this->insert_transaction( $entry['id'], 'payment', $payment['transaction_id'], $payment['amount'], false, rgar( $subscription, 'subscription_id' ) ); $amount_formatted = GFCommon::to_money( $payment['amount'], $entry['currency'] ); $note = sprintf( esc_html__( '%s has been captured successfully. Amount: %s. Transaction Id: %s', 'gravityforms' ), $payment_name, $amount_formatted, $payment['transaction_id'] ); $this->add_note( $entry['id'], $note, 'success' ); } elseif ( $payment && ! $payment['is_success'] ) { $this->add_note( $entry['id'], sprintf( esc_html__( 'Failed to capture %s. Reason: %s.', 'gravityforms' ), $payment['error_message'], $payment_name ), 'error' ); } // Updating subscription information. if ( $subscription['is_success'] ) { $entry = $this->start_subscription( $entry, $subscription ); } else { $entry['payment_status'] = 'Failed'; GFAPI::update_entry( $entry ); $this->add_note( $entry['id'], sprintf( esc_html__( 'Subscription failed to be created. Reason: %s', 'gravityforms' ), $subscription['error_message'] ), 'error' ); $subscription['type'] = 'fail_create_subscription'; $this->post_payment_action( $entry, $subscription ); } return $entry; } /** * Inserts a new transaction item. * * @since Unknown * @access public * * @used-by GFPaymentAddOn::add_subscription_payment() * @used-by GFPaymentAddOn::complete_authorization() * @used-by GFPaymentAddOn::process_subscription() * @used-by GFPaymentAddOn::refund_payment() * @uses wpdb::get_var() * @uses wpdb::prepare() * @uses wpdb::query() * @uses wpdb::$insert_id * * @global wpdb $wpdb The wpdb object. * @param int $entry_id The entry ID that contains the transaction. * @param string $transaction_type The transaction type. * @param string $transaction_id The ID of the transaction to be inserted. * @param float $amount The transaction amount. * @param int|null $is_recurring If the transaction is recurring. Defaults to null. * @param string|null $subscription_id The subscription ID tied to the transaction, if related to a subscription. * Defaults to null. * * @return int|WP_Error The row ID from the database entry. WP_Error if error. */ public function insert_transaction( $entry_id, $transaction_type, $transaction_id, $amount, $is_recurring = null, $subscription_id = null ) { global $wpdb; // @todo: make sure stats does not show setup fee as a recurring payment $payment_count = $wpdb->get_var( $wpdb->prepare( "SELECT count(id) FROM {$wpdb->prefix}gf_addon_payment_transaction WHERE lead_id=%d", $entry_id ) ); $is_recurring = $payment_count > 0 && $transaction_type == 'payment' ? 1 : 0; $subscription_id = empty( $subscription_id ) ? '' : $subscription_id; $sql = $wpdb->prepare( " INSERT INTO {$wpdb->prefix}gf_addon_payment_transaction (lead_id, transaction_type, transaction_id, amount, is_recurring, date_created, subscription_id) values(%d, %s, %s, %f, %d, utc_timestamp(), %s)", $entry_id, $transaction_type, $transaction_id, $amount, $is_recurring, $subscription_id ); $wpdb->query( $sql ); $txn_id = $wpdb->insert_id; /** * Fires after a payment transaction is created in Gravity Forms. * * @since Unknown * * @param int $txn_id The overall Transaction ID. * @param int $entry_id The new Entry ID. * @param string $transaction_type The Type of transaction that was made. * @param int $transaction_id The transaction ID. * @param string $amount The amount payed in the transaction. * @param bool $is_recurring True or false if this is an ongoing payment. */ do_action( 'gform_post_payment_transaction', $txn_id, $entry_id, $transaction_type, $transaction_id, $amount, $is_recurring, $subscription_id ); if ( has_filter( 'gform_post_payment_transaction' ) ) { $this->log_debug( __METHOD__ . '(): Executing functions hooked to gform_post_payment_transaction.' ); } return $txn_id; } /** * Gets the payment submission feed. * * @since Unknown * @access public * * @used-by GFPaymentAddOn::ajax_cancel_subscription() * @used-by GFPaymentAddOn::process_callback_action() * @used-by GFPaymentAddOn::validation() * @uses GFFeedAddOn::get_feeds_by_entry() * @uses GFFeedAddOn::get_feed() * @uses GFFeedAddOn::get_feeds() * @uses GFFeedAddOn::pre_process_feeds() * @uses GFFeedAddOn::is_feed_condition_met() * * @param array $entry The Entry Object. * @param bool|array $form The Form Object. Defaults to false. * * @return array The submission feed. */ public function get_payment_feed( $entry, $form = false ) { $submission_feed = false; // Only occurs if entry has already been processed and feed has been stored in entry meta. if ( $entry['id'] ) { $feeds = $this->get_feeds_by_entry( $entry['id'] ); $submission_feed = empty( $feeds ) ? false : $this->get_feed( $feeds[0] ); } elseif ( $form ) { // Getting all feeds. $feeds = $this->get_feeds( $form['id'] ); $feeds = $this->pre_process_feeds( $feeds, $entry, $form ); foreach ( $feeds as $feed ) { if ( $feed['is_active'] && $this->is_feed_condition_met( $feed, $form, $entry ) ) { $submission_feed = $feed; break; } } } return $submission_feed; } /** * Determines if this is a payment gateway add-on. * * @since Unknown * @access public * * @used-by GFPaymentAddOn::entry_info() * @uses GFPaymentAddOn::$is_payment_gateway() * @uses GFAddOn::$_slug * * @param int $entry_id The entry ID. * * @return bool True if it is a payment gateway. False otherwise. */ public function is_payment_gateway( $entry_id ) { if ( $this->is_payment_gateway ) { return true; } $gateway = gform_get_meta( $entry_id, 'payment_gateway' ); return $gateway == $this->_slug; } /** * Gets the payment submission data. * * @since Unknown * @access public * * @used-by GFPaymentAddOn::validation() * @uses GFPaymentAddOn::billing_info_fields() * @uses GFPaymentAddOn::get_credit_card_field() * @uses GFAddOn::get_field_value() * @uses GFPaymentAddOn::remove_spaces_from_card_number() * @uses GFPaymentAddOn::get_order_data() * * @param array $feed The Feed Object. * @param array $form The Form Object. * @param array $entry The Entry Object. * * @return array The payment submission data. */ public function get_submission_data( $feed, $form, $entry ) { $submission_data = array(); if ( empty( $feed['meta'] ) ) { return $submission_data; } $submission_data['form_title'] = $form['title']; // Getting mapped field data. $billing_fields = $this->billing_info_fields(); foreach ( $billing_fields as $billing_field ) { $field_name = $billing_field['name']; $input_id = rgar( $feed['meta'], "billingInformation_{$field_name}" ); $submission_data[ $field_name ] = $this->get_field_value( $form, $entry, $input_id ); } // Getting credit card field data. $card_field = $this->get_credit_card_field( $form ); if ( $card_field ) { $submission_data['card_number'] = $this->remove_spaces_from_card_number( rgpost( "input_{$card_field->id}_1" ) ); $submission_data['card_expiration_date'] = rgpost( "input_{$card_field->id}_2" ); $submission_data['card_security_code'] = rgpost( "input_{$card_field->id}_3" ); $submission_data['card_name'] = rgpost( "input_{$card_field->id}_5" ); } // Getting product field data. $order_info = $this->get_order_data( $feed, $form, $entry ); $submission_data = array_merge( $submission_data, $order_info ); /** * Enables the Submission Data to be modified before it is used during feed processing by the payment add-on. * * @since 1.9.12.8 * * @param array $submission_data The customer and transaction data. * @param array $feed The Feed Object. * @param array $form The Form Object. * @param array $entry The Entry Object. * * @return array $submission_data */ return gf_apply_filters( array( 'gform_submission_data_pre_process_payment', $form['id'] ), $submission_data, $feed, $form, $entry ); } /** * Gets the credit card field object. * * @since Unknown * @access public * * @used-by GFPaymentAddOn::before_delete_field() * @used-by GFPaymentAddOn::get_submission_data() * @used-by GFPaymentAddOn::has_credit_card_field() * @uses GFAPI::get_fields_by_type() * * @param array $form The Form Object. * * @return bool|GF_Field_CreditCard The credit card field object, if found. Otherwise, false. */ public function get_credit_card_field( $form ) { $fields = GFAPI::get_fields_by_type( $form, array( 'creditcard' ) ); return empty( $fields ) ? false : $fields[0]; } /** * Checks if a form has a credit card field. * * @since Unknown * @access public * * @used-by GFPaymentAddOn::feed_list_message() * @uses GFPaymentAddOn::get_credit_card_field() * * @param array $form The Form Object. * * @return bool True if the form has a credit card field. False otherwise. */ public function has_credit_card_field( $form ) { return $this->get_credit_card_field( $form ) !== false; } /** * Gets payment order data. * * @since Unknown * @access public * * @used-by GFPaymentAddOn::get_submission_data() * @uses GFCommon::get_product_fields() * @uses GFCommon::to_number() * * @param array $feed The Feed Object. * @param array $form The Form Object. * @param array $entry The Entry Object. * * @return array { * The order data. * * @type float $payment_amount The payment amount of the order. * @type float $setup_fee The setup fee, if any. * @type float $trial The trial fee, if any. * @type float $discounts Discounts applied, if any. * } */ public function get_order_data( $feed, $form, $entry ) { $products = GFCommon::get_product_fields( $form, $entry ); $payment_field = $this->get_payment_field( $feed ); $setup_fee_field = rgar( $feed['meta'], 'setupFee_enabled' ) ? $feed['meta']['setupFee_product'] : false; $trial_field = rgar( $feed['meta'], 'trial_enabled' ) ? rgars( $feed, 'meta/trial_product' ) : false; $amount = 0; $line_items = array(); $discounts = array(); $fee_amount = 0; $trial_amount = 0; foreach ( $products['products'] as $field_id => $product ) { $quantity = $product['quantity'] ? $product['quantity'] : 1; $product_price = GFCommon::to_number( $product['price'], $entry['currency'] ); $options = array(); if ( is_array( rgar( $product, 'options' ) ) ) { foreach ( $product['options'] as $option ) { $options[] = $option['option_name']; $product_price += $option['price']; } } $is_trial_or_setup_fee = false; if ( ! empty( $trial_field ) && $trial_field == $field_id ) { $trial_amount = $product_price * $quantity; $is_trial_or_setup_fee = true; } elseif ( ! empty( $setup_fee_field ) && $setup_fee_field == $field_id ) { $fee_amount = $product_price * $quantity; $is_trial_or_setup_fee = true; } // Do not add to line items if the payment field selected in the feed is not the current field. if ( is_numeric( $payment_field ) && $payment_field != $field_id ) { continue; } // Do not add to line items if the payment field is set to "Form Total" and the current field was used for trial or setup fee. if ( $is_trial_or_setup_fee && ! is_numeric( $payment_field ) ) { continue; } $amount += $product_price * $quantity; $description = ''; if ( ! empty( $options ) ) { $description = esc_html__( 'options: ', 'gravityforms' ) . ' ' . implode( ', ', $options ); } if ( $product_price >= 0 ) { $line_items[] = array( 'id' => $field_id, 'name' => $product['name'], 'description' => $description, 'quantity' => $quantity, 'unit_price' => GFCommon::to_number( $product_price, $entry['currency'] ), 'options' => rgar( $product, 'options' ) ); } else { $discounts[] = array( 'id' => $field_id, 'name' => $product['name'], 'description' => $description, 'quantity' => $quantity, 'unit_price' => GFCommon::to_number( $product_price, $entry['currency'] ), 'options' => rgar( $product, 'options' ) ); } } if ( $trial_field == 'enter_amount' ) { $trial_amount = rgar( $feed['meta'], 'trial_amount' ) ? GFCommon::to_number( rgar( $feed['meta'], 'trial_amount' ), $entry['currency'] ) : 0; } if ( ! empty( $products['shipping']['name'] ) && ! is_numeric( $payment_field ) ) { $line_items[] = array( 'id' => $products['shipping']['id'], 'name' => $products['shipping']['name'], 'description' => '', 'quantity' => 1, 'unit_price' => GFCommon::to_number( $products['shipping']['price'], $entry['currency'] ), 'is_shipping' => 1 ); $amount += $products['shipping']['price']; } return array( 'payment_amount' => $amount, 'setup_fee' => $fee_amount, 'trial' => $trial_amount, 'line_items' => $line_items, 'discounts' => $discounts ); } /** * Returns what should be used to prepare the payment amount; the form_total or the ID of a specific product field. * * Override if your add-on uses custom choices for the transactionType setting or does not use the standard recurringAmount and paymentAmount settings. * * @since 2.4.17 * * @param array $feed The current feed. * * @return string */ public function get_payment_field( $feed ) { $key = rgars( $feed, 'meta/transactionType' ) === 'subscription' ? 'recurringAmount' : 'paymentAmount'; return rgars( $feed, 'meta/' . $key, 'form_total' ); } /** * Checks if the callback should be processed by this payment add-on. * * @since Unknown * @access public * * @used-by GFPaymentAddOn::maybe_process_callback() * @uses GFAddOn::$_slug * * @return bool True if valid. False otherwise. */ public function is_callback_valid() { if ( rgget( 'callback' ) != $this->_slug ) { return false; } return true; } //--------- Callback (aka Webhook)---------------- /** * Conditionally initiates processing of the callback. * * Checks to see if the callback is valid, processes callback actions, then returns the appropriate response. * * @since Unknown * @access public * * @used-by GFPaymentAddOn::pre_init() * @uses GFPaymentAddOn::is_callback_valid() * @uses GFAddOn::$_slug * @uses GFPaymentAddOn::callback() * @uses GFPaymentAddOn::display_callback_error() * @uses GFPaymentAddOn::process_callback_action() * @uses GFPaymentAddOn::post_callback() * * @return void */ public function maybe_process_callback() { // Ignoring requests that are not this addon's callbacks. if ( ! $this->is_callback_valid() ) { return; } // Returns either false or an array of data about the callback request which payment add-on will then use // to generically process the callback data $this->log_debug( __METHOD__ . '(): Initializing callback processing for: ' . $this->_slug ); $callback_action = $this->callback(); $this->log_debug( __METHOD__ . '(): Result from gateway callback => ' . print_r( $callback_action, true ) ); $result = false; if ( is_wp_error( $callback_action ) ) { $this->display_callback_error( $callback_action ); } elseif ( $callback_action && is_array( $callback_action ) && rgar( $callback_action, 'type' ) && ! rgar( $callback_action, 'abort_callback' ) ) { $result = $this->process_callback_action( $callback_action ); $this->log_debug( __METHOD__ . '(): Result of callback action => ' . print_r( $result, true ) ); if ( is_wp_error( $result ) ) { $this->display_callback_error( $result ); } elseif ( ! $result ) { status_header( 200 ); echo 'Callback could not be processed.'; } else { status_header( 200 ); echo 'Callback processed successfully.'; } } else { status_header( 200 ); echo 'Callback bypassed'; } $this->post_callback( $callback_action, $result ); die(); } /** * Displays a callback error, if needed. * * @since Unknown * @access public * * @uses WP_Error::get_error_data() * @uses WP_Error::get_error_message() * * @param WP_Error $error The error. * * @return void */ private function display_callback_error( $error ) { $data = $error->get_error_data(); $status = ! rgempty( 'status_header', $data ) ? $data['status_header'] : 200; status_header( $status ); echo $error->get_error_message(); } /** * Processes callback based on provided data. * * @since Unknown * @access private * * @uses GFPaymentAddOn::is_duplicate_callback() * @uses GFAPI::get_entry() * @uses GFPaymentAddOn::complete_payment() * @uses GFPaymentAddOn::refund_payment() * @uses GFPaymentAddOn::fail_payment() * @uses GFPaymentAddOn::add_pending_payment() * @uses GFPaymentAddOn::void_authorization() * @uses GFPaymentAddOn::start_subscription() * @uses GFPaymentAddOn::get_payment_feed() * @uses GFPaymentAddOn::cancel_subscription() * @uses GFPaymentAddOn::expire_subscription() * @uses GFPaymentAddOn::add_subscription_payment() * @uses GFPaymentAddOn::fail_subscription_payment() * @uses GFPaymentAddOn::register_callback() * * @param array $action { * The action to perform. * * @type string $type The callback action type. Required. * @type string $transaction_id The transaction ID to perform the action on. Required if the action is a payment. * @type string $subscription_id The subscription ID. Required if this is related to a subscription. * @type string $amount The transaction amount. Typically required. * @type int $entry_id The ID of the entry associated with the action. Typically required. * @type string $transaction_type The transaction type to process this action as. Optional. * @type string $payment_status The payment status to set the payment to. Optional. * @type string $note The note to associate with this payment action. Optional. * } * * @return bool|mixed True, unless a custom transaction type defines otherwise. */ private function process_callback_action( $action ) { $this->log_debug( __METHOD__ . '(): Processing callback action.' ); $action = wp_parse_args( $action, array( 'type' => false, 'amount' => false, 'amount_formatted' => false, 'transaction_type' => false, 'transaction_id' => false, 'subscription_id' => false, 'entry_id' => false, 'payment_status' => false, 'note' => false, ) ); $result = false; if ( rgar( $action, 'id' ) && $this->is_duplicate_callback( $action['id'] ) ) { return new WP_Error( 'duplicate', sprintf( esc_html__( 'This webhook has already been processed (Event Id: %s)', 'gravityforms' ), $action['id'] ) ); } $entry = GFAPI::get_entry( $action['entry_id'] ); if ( ! $entry || is_wp_error( $entry ) ) { return $result; } $action = $this->maybe_add_action_amount_formatted( $action, $entry['currency'] ); /** * Performs actions before the the payment action callback is processed. * * @since Unknown * * @param array $action The action array. * @param array $entry The Entry Object. */ do_action( 'gform_action_pre_payment_callback', $action, $entry ); if ( has_filter( 'gform_action_pre_payment_callback' ) ) { $this->log_debug( __METHOD__ . '(): Executing functions hooked to gform_action_pre_payment_callback.' ); } switch ( $action['type'] ) { case 'complete_payment': $result = $this->complete_payment( $entry, $action ); break; case 'refund_payment': $result = $this->refund_payment( $entry, $action ); break; case 'fail_payment': $result = $this->fail_payment( $entry, $action ); break; case 'add_pending_payment': $result = $this->add_pending_payment( $entry, $action ); break; case 'void_authorization': $result = $this->void_authorization( $entry, $action ); break; case 'create_subscription': $result = $this->start_subscription( $entry, $action ); $result = rgar( $result, 'payment_status' ) == 'Active' && rgar( $result, 'transaction_id' ) == rgar( $action, 'subscription_id' ); break; case 'cancel_subscription': $feed = $this->get_payment_feed( $entry ); $result = $this->cancel_subscription( $entry, $feed, $action['note'] ); break; case 'expire_subscription': $result = $this->expire_subscription( $entry, $action ); break; case 'add_subscription_payment': $result = $this->add_subscription_payment( $entry, $action ); break; case 'fail_subscription_payment': $result = $this->fail_subscription_payment( $entry, $action ); break; default: // Handle custom events. if ( is_callable( array( $this, rgar( $action, 'callback' ) ) ) ) { $result = call_user_func_array( array( $this, $action['callback'] ), array( $entry, $action ) ); } break; } if ( rgar( $action, 'id' ) && $result ) { $this->register_callback( $action['id'], $action['entry_id'] ); } /** * Fires right after the payment callback. * * @since Unknown * * @param array $entry The Entry Object * @param array $action { * The action performed. * * @type string $type The callback action type. Required. * @type string $transaction_id The transaction ID to perform the action on. Required if the action is a payment. * @type string $subscription_id The subscription ID. Required if this is related to a subscription. * @type string $amount The transaction amount. Typically required. * @type int $entry_id The ID of the entry associated with the action. Typically required. * @type string $transaction_type The transaction type to process this action as. Optional. * @type string $payment_status The payment status to set the payment to. Optional. * @type string $note The note to associate with this payment action. Optional. * } * @param mixed $result The Result Object. */ do_action( 'gform_post_payment_callback', $entry, $action, $result ); if ( has_filter( 'gform_post_payment_callback' ) ) { $this->log_debug( __METHOD__ . '(): Executing functions hooked to gform_post_payment_callback.' ); } return $result; } /** * Registers a callback action. * * @since Unknown * @access public * * @uses wpdb::insert() * @uses GFAddOn::$_slug * * @global wpdb $wpdb * @param string $callback_id The callback ID for the action. * @param int $entry_id The entry ID associated with the callback. * * @return void */ public function register_callback( $callback_id, $entry_id ) { global $wpdb; $wpdb->insert( "{$wpdb->prefix}gf_addon_payment_callback", array( 'addon_slug' => $this->_slug, 'callback_id' => $callback_id, 'lead_id' => $entry_id, 'date_created' => gmdate( 'Y-m-d H:i:s' ) ) ); } /** * Checks if a callback is duplicate. * * @since Unknown * @access public * * @uses wpdb::$prefix * @uses wpdb::prepare() * @uses wpdb::get_var() * * @global wpdb $wpdb * @param string $callback_id The callback ID to chack. * * @return bool If the callback is a duplicate, true. Otherwise, false. */ public function is_duplicate_callback( $callback_id ) { global $wpdb; $sql = $wpdb->prepare( "SELECT id FROM {$wpdb->prefix}gf_addon_payment_callback WHERE addon_slug=%s AND callback_id=%s", $this->_slug, $callback_id ); if ( $wpdb->get_var( $sql ) ) { return true; } return false; } public function callback() { } public function post_callback( $callback_action, $result ) { } // # PAYMENT INTERACTION FUNCTIONS public function add_pending_payment( $entry, $action ) { $this->log_debug( __METHOD__ . '(): Processing request.' ); if ( empty( $action['payment_status'] ) ) { $action['payment_status'] = 'Pending'; } $action = $this->maybe_add_action_amount_formatted( $action, $entry['currency'] ); if ( empty( $action['note'] ) ) { $action['note'] = sprintf( esc_html__( 'Payment is pending. Amount: %s. Transaction Id: %s.', 'gravityforms' ), $action['amount_formatted'], $action['transaction_id'] ); } GFAPI::update_entry_property( $entry['id'], 'payment_status', $action['payment_status'] ); $this->add_note( $entry['id'], $action['note'] ); $this->post_payment_action( $entry, $action ); return true; } public function complete_authorization( &$entry, $action ) { $this->log_debug( __METHOD__ . '(): Processing request.' ); if ( ! rgar( $action, 'payment_status' ) ) { $action['payment_status'] = 'Authorized'; } if ( ! rgar( $action, 'transaction_type' ) ) { $action['transaction_type'] = 'authorization'; } if ( ! rgar( $action, 'payment_date' ) ) { $action['payment_date'] = gmdate( 'y-m-d H:i:s' ); } $entry['transaction_id'] = rgar( $action, 'transaction_id' ); $entry['transaction_type'] = '1'; $entry['payment_status'] = $action['payment_status']; $action = $this->maybe_add_action_amount_formatted( $action, $entry['currency'] ); if ( ! rgar( $action, 'note' ) ) { $action['note'] = sprintf( esc_html__( 'Payment has been authorized. Amount: %s. Transaction Id: %s.', 'gravityforms' ), $action['amount_formatted'], $action['transaction_id'] ); } GFAPI::update_entry( $entry ); $this->add_note( $entry['id'], $action['note'], 'success' ); $this->post_payment_action( $entry, $action ); return true; } public function complete_payment( &$entry, $action ) { $this->log_debug( __METHOD__ . '(): Processing request.' ); if ( ! rgar( $action, 'payment_status' ) ) { $action['payment_status'] = 'Paid'; } if ( ! rgar( $action, 'transaction_type' ) ) { $action['transaction_type'] = 'payment'; } if ( ! rgar( $action, 'payment_date' ) ) { $action['payment_date'] = gmdate( 'y-m-d H:i:s' ); } $entry['is_fulfilled'] = '1'; $entry['transaction_id'] = rgar( $action, 'transaction_id' ); $entry['transaction_type'] = '1'; $entry['payment_status'] = $action['payment_status']; $entry['payment_amount'] = rgar( $action, 'amount' ); $entry['payment_date'] = $action['payment_date']; $entry['payment_method'] = rgar( $action, 'payment_method' ); $action = $this->maybe_add_action_amount_formatted( $action, $entry['currency'] ); if ( ! rgar( $action, 'note' ) ) { $action['note'] = sprintf( esc_html__( 'Payment has been completed. Amount: %s. Transaction Id: %s.', 'gravityforms' ), $action['amount_formatted'], $action['transaction_id'] ); } GFAPI::update_entry( $entry ); $this->insert_transaction( $entry['id'], $action['transaction_type'], $action['transaction_id'], $action['amount'] ); $this->add_note( $entry['id'], $action['note'], 'success' ); /** * Fires after a payment is completed through a form * * @param array $entry The Entry object * @param array $action The Action Object * $action = array( * 'type' => 'cancel_subscription', // See Below * 'transaction_id' => '', // What is the ID of the transaction made? * 'subscription_id' => '', // What is the ID of the Subscription made? * 'amount' => '0.00', // Amount to charge? * 'entry_id' => 1, // What entry to check? * 'transaction_type' => '', * 'payment_status' => '', * 'note' => '' * ); * * 'type' can be: * * - complete_payment * - refund_payment * - fail_payment * - add_pending_payment * - void_authorization * - create_subscription * - cancel_subscription * - expire_subscription * - add_subscription_payment * - fail_subscription_payment */ do_action( 'gform_post_payment_completed', $entry, $action ); if ( has_filter( 'gform_post_payment_completed' ) ) { $this->log_debug( __METHOD__ . '(): Executing functions hooked to gform_post_payment_completed.' ); } $this->post_payment_action( $entry, $action ); return true; } public function refund_payment( $entry, $action ) { $this->log_debug( __METHOD__ . '(): Processing request.' ); if ( empty( $action['payment_status'] ) ) { $action['payment_status'] = 'Refunded'; } if ( empty( $action['transaction_type'] ) ) { $action['transaction_type'] = 'refund'; } $action = $this->maybe_add_action_amount_formatted( $action, $entry['currency'] ); if ( empty( $action['note'] ) ) { $action['note'] = sprintf( esc_html__( 'Payment has been refunded. Amount: %s. Transaction Id: %s.', 'gravityforms' ), $action['amount_formatted'], $action['transaction_id'] ); } GFAPI::update_entry_property( $entry['id'], 'payment_status', $action['payment_status'] ); $this->insert_transaction( $entry['id'], $action['transaction_type'], $action['transaction_id'], $action['amount'] ); $this->add_note( $entry['id'], $action['note'] ); /** * Fires after a payment is refunded * * @param array $entry The Entry object * @param array $action The Action Object * $action = array( * 'type' => 'cancel_subscription', // See Below * 'transaction_id' => '', // What is the ID of the transaction made? * 'subscription_id' => '', // What is the ID of the Subscription made? * 'amount' => '0.00', // Amount to charge? * 'entry_id' => 1, // What entry to check? * 'transaction_type' => '', * 'payment_status' => '', * 'note' => '' * ); * * 'type' can be: * * - complete_payment * - refund_payment * - fail_payment * - add_pending_payment * - void_authorization * - create_subscription * - cancel_subscription * - expire_subscription * - add_subscription_payment * - fail_subscription_payment */ do_action( 'gform_post_payment_refunded', $entry, $action ); if ( has_filter( 'gform_post_payment_refunded' ) ) { $this->log_debug( __METHOD__ . '(): Executing functions hooked to gform_post_payment_refunded.' ); } $this->post_payment_action( $entry, $action ); return true; } public function fail_payment( $entry, $action ) { $this->log_debug( __METHOD__ . '(): Processing request.' ); if ( empty( $action['payment_status'] ) ) { $action['payment_status'] = 'Failed'; } $action = $this->maybe_add_action_amount_formatted( $action, $entry['currency'] ); if ( empty( $action['note'] ) ) { $action['note'] = sprintf( esc_html__( 'Payment has failed. Amount: %s.', 'gravityforms' ), $action['amount_formatted'] ); } GFAPI::update_entry_property( $entry['id'], 'payment_status', $action['payment_status'] ); $this->add_note( $entry['id'], $action['note'] ); $this->post_payment_action( $entry, $action ); return true; } public function void_authorization( $entry, $action ) { $this->log_debug( __METHOD__ . '(): Processing request.' ); if ( empty( $action['payment_status'] ) ) { $action['payment_status'] = 'Voided'; } $action = $this->maybe_add_action_amount_formatted( $action, $entry['currency'] ); if ( empty( $action['note'] ) ) { $action['note'] = sprintf( esc_html__( 'Authorization has been voided. Transaction Id: %s', 'gravityforms' ), $action['transaction_id'] ); } GFAPI::update_entry_property( $entry['id'], 'payment_status', $action['payment_status'] ); $this->add_note( $entry['id'], $action['note'] ); $this->post_payment_action( $entry, $action ); return true; } /** * Used to start a new subscription. Updates the associcated entry with the payment and transaction details and adds an entry note. * * @param [array] $entry Entry object * @param [string] $subscription_id ID of the subscription * @param [float] $amount Numeric amount of the initial subscription payment * * @return [array] $entry Entry Object */ public function start_subscription( $entry, $subscription ) { $this->log_debug( __METHOD__ . '(): Processing request.' ); if ( ! $this->has_subscription( $entry ) ) { $entry['payment_status'] = 'Active'; $entry['payment_amount'] = $subscription['amount']; $entry['payment_date'] = ! rgempty( 'subscription_start_date', $subscription ) ? $subscription['subscription_start_date'] : gmdate( 'Y-m-d H:i:s' ); $entry['transaction_id'] = $subscription['subscription_id']; $entry['transaction_type'] = '2'; // subscription $entry['is_fulfilled'] = '1'; $result = GFAPI::update_entry( $entry ); $this->add_note( $entry['id'], sprintf( esc_html__( 'Subscription has been created. Subscription Id: %s.', 'gravityforms' ), $subscription['subscription_id'] ), 'success' ); $subscription = $this->maybe_add_action_amount_formatted( $subscription, $entry['currency'] ); if ( empty( $subscription['payment_status'] ) ) { $subscription['payment_status'] = 'Active'; } /** * Fires when someone starts a subscription * * @param array $entry Entry Object * @param array $subscription The new Subscription object */ do_action( 'gform_post_subscription_started', $entry, $subscription ); if ( has_filter( 'gform_post_subscription_started' ) ) { $this->log_debug( __METHOD__ . '(): Executing functions hooked to gform_post_subscription_started.' ); } $subscription['type'] = 'create_subscription'; $this->post_payment_action( $entry, $subscription ); } return $entry; } /** * A payment on an existing subscription. * * @param [array] $data Transaction data including 'amount' and 'subscriber_id' * @param [array] $entry Entry object * * @return true */ public function add_subscription_payment( $entry, $action ) { $this->log_debug( __METHOD__ . '(): Processing request.' ); if ( empty( $action['transaction_type'] ) ) { $action['transaction_type'] = 'payment'; } if ( empty( $action['payment_status'] ) ) { $action['payment_status'] = 'Active'; } // Set payment status back to active if a previous payment attempt failed. if ( strtolower( $entry['payment_status'] ) != 'active' ) { $entry['payment_status'] = 'Active'; GFAPI::update_entry_property( $entry['id'], 'payment_status', 'Active' ); } $action = $this->maybe_add_action_amount_formatted( $action, $entry['currency'] ); if ( empty( $action['note'] ) ) { $action['note'] = sprintf( esc_html__( 'Subscription has been paid. Amount: %s. Subscription Id: %s', 'gravityforms' ), $action['amount_formatted'], $action['subscription_id'] ); } $transaction_id = ! empty( $action['transaction_id'] ) ? $action['transaction_id'] : $action['subscription_id']; $this->insert_transaction( $entry['id'], $action['transaction_type'], $transaction_id, $action['amount'], null, rgar( $action, 'subscription_id') ); $this->add_note( $entry['id'], $action['note'], 'success' ); /** * Fires after a payment is made on an existing subscription. * * @param array $entry The Entry Object * @param array $action The Action Object * $action = array( * 'type' => 'cancel_subscription', // See Below * 'transaction_id' => '', // What is the ID of the transaction made? * 'subscription_id' => '', // What is the ID of the Subscription made? * 'amount' => '0.00', // Amount to charge? * 'entry_id' => 1, // What entry to check? * 'transaction_type' => '', * 'payment_status' => '', * 'note' => '' * ); * * 'type' can be: * * - complete_payment * - refund_payment * - fail_payment * - add_pending_payment * - void_authorization * - create_subscription * - cancel_subscription * - expire_subscription * - add_subscription_payment * - fail_subscription_payment */ do_action( 'gform_post_add_subscription_payment', $entry, $action ); if ( has_filter( 'gform_post_add_subscription_payment' ) ) { $this->log_debug( __METHOD__ . '(): Executing functions hooked to gform_post_add_subscription_payment.' ); } $this->post_payment_action( $entry, $action ); return true; } public function fail_subscription_payment( $entry, $action ) { $this->log_debug( __METHOD__ . '(): Processing request.' ); if ( empty( $action['payment_status'] ) ) { $action['payment_status'] = 'Failed'; } $action = $this->maybe_add_action_amount_formatted( $action, $entry['currency'] ); if ( empty( $action['note'] ) ) { $action['note'] = sprintf( esc_html__( 'Subscription payment has failed. Amount: %s. Subscription Id: %s.', 'gravityforms' ), $action['amount_formatted'], $action['subscription_id'] ); } GFAPI::update_entry_property( $entry['id'], 'payment_status', 'Failed' ); $this->add_note( $entry['id'], $action['note'], 'error' ); // keep 'gform_subscription_payment_failed' for backward compatability /** * @deprecated Use gform_post_fail_subscription_payment now */ do_action( 'gform_subscription_payment_failed', $entry, $action['subscription_id'] ); if ( has_filter( 'gform_subscription_payment_failed' ) ) { $this->log_debug( __METHOD__ . '(): Executing functions hooked to gform_subscription_payment_failed.' ); } /** * Fires after a subscription payment has failed * * @param array $entry The Entry Object * @param array $action The Action Object * $action = array( * 'type' => 'cancel_subscription', // See Below * 'transaction_id' => '', // What is the ID of the transaction made? * 'subscription_id' => '', // What is the ID of the Subscription made? * 'amount' => '0.00', // Amount to charge? * 'entry_id' => 1, // What entry to check? * 'transaction_type' => '', * 'payment_status' => '', * 'note' => '' * ); * * 'type' can be: * * - complete_payment * - refund_payment * - fail_payment * - add_pending_payment * - void_authorization * - create_subscription * - cancel_subscription * - expire_subscription * - add_subscription_payment * - fail_subscription_payment */ do_action( 'gform_post_fail_subscription_payment', $entry, $action ); if ( has_filter( 'gform_post_fail_subscription_payment' ) ) { $this->log_debug( __METHOD__ . '(): Executing functions hooked to gform_post_fail_subscription_payment.' ); } $this->post_payment_action( $entry, $action ); return true; } public function cancel_subscription( $entry, $feed, $note = null ) { $this->log_debug( __METHOD__ . '(): Processing request.' ); if ( ! $note ) { $note = sprintf( esc_html__( 'Subscription has been cancelled. Subscription Id: %s.', 'gravityforms' ), $entry['transaction_id'] ); } if ( strtolower( $entry['payment_status'] ) == 'cancelled' ) { $this->log_debug( __METHOD__ . '(): Subscription is already canceled.' ); return false; } GFAPI::update_entry_property( $entry['id'], 'payment_status', 'Cancelled' ); $this->add_note( $entry['id'], $note ); // Include $subscriber_id as 3rd parameter for backwards compatibility do_action( 'gform_subscription_canceled', $entry, $feed, $entry['transaction_id'] ); // Include alternative spelling of "cancelled". do_action( 'gform_subscription_cancelled', $entry, $feed, $entry['transaction_id'] ); if ( has_filter( 'gform_subscription_canceled' ) || has_filter( 'gform_subscription_cancelled' ) ) { $this->log_debug( __METHOD__ . '(): Executing functions hooked to gform_subscription_canceled.' ); } $action = array( 'type' => 'cancel_subscription', 'subscription_id' => $entry['transaction_id'], 'entry_id' => $entry['id'], 'payment_status' => 'Cancelled', 'note' => $note, ); $this->post_payment_action( $entry, $action ); return true; } public function expire_subscription( $entry, $action ) { $this->log_debug( __METHOD__ . '(): Processing request.' ); if ( empty( $action['payment_status'] ) ) { $action['payment_status'] = 'Expired'; } if ( empty( $action['note'] ) ) { $action['note'] = sprintf( esc_html__( 'Subscription has expired. Subscriber Id: %s', 'gravityforms' ), $action['subscription_id'] ); } GFAPI::update_entry_property( $entry['id'], 'payment_status', 'Expired' ); $this->add_note( $entry['id'], $action['note'] ); $this->post_payment_action( $entry, $action ); return true; } public function has_subscription( $entry ) { if ( rgar( $entry, 'transaction_type' ) == 2 && ! rgempty( 'transaction_id', $entry ) ) { return true; } else { return false; } } /** * Retrieves the ID of the entry associated with the supplied subscription or transaction ID. * * @since 2.3.3.9 Updated to search the _gf_addon_payment_transaction table if the ID was not found in the entry table. * @since unknown * * @param string $transaction_id The subscription or transaction ID. * * @return bool|string */ public function get_entry_by_transaction_id( $transaction_id ) { if ( empty( $transaction_id ) ) { return false; } global $wpdb; $entry_table_name = self::get_entry_table_name(); $sql = $wpdb->prepare( "SELECT id FROM {$entry_table_name} WHERE transaction_id = %s", $transaction_id ); $entry_id = $wpdb->get_var( $sql ); if ( ! $entry_id ) { $sql = $wpdb->prepare( "SELECT lead_id FROM {$wpdb->prefix}gf_addon_payment_transaction WHERE transaction_id = %s", $transaction_id ); $entry_id = $wpdb->get_var( $sql ); } return $entry_id ? $entry_id : false; } /** * Helper for making the gform_post_payment_action hook available to the various payment interaction methods. Also handles sending notifications for payment events. * * @since 2.3.6.6 Added the $action to the GFAPI::send_notifications() $data param. * @since unknown * * @param array $entry * @param array $action */ public function post_payment_action( $entry, $action ) { do_action( 'gform_post_payment_action', $entry, $action ); if ( has_filter( 'gform_post_payment_action' ) ) { $this->log_debug( __METHOD__ . '(): Executing functions hooked to gform_post_payment_action.' ); } $form = GFAPI::get_form( $entry['form_id'] ); $supported_events = $this->supported_notification_events( $form ); if ( ! empty( $supported_events ) ) { if ( ! empty( $action['payment_status'] ) ) { $action['payment_status'] = GFCommon::get_entry_payment_status_text( $action['payment_status'] ); } GFAPI::send_notifications( $form, $entry, rgar( $action, 'type' ), array( 'payment_action' => $action ) ); } } // -------- Cron -------------------- public function setup_cron() { // Setting up cron $cron_name = "{$this->_slug}_cron"; add_action( $cron_name, array( $this, 'check_status' ) ); if ( ! wp_next_scheduled( $cron_name ) ) { wp_schedule_event( time(), 'hourly', $cron_name ); } } public function check_status() { } //--------- List Columns ------------ public function feed_list_columns() { return array( 'feedName' => esc_html__( 'Name', 'gravityforms' ), 'transactionType' => esc_html__( 'Transaction Type', 'gravityforms' ), 'amount' => esc_html__( 'Amount', 'gravityforms' ) ); } public function get_column_value_transactionType( $feed ) { switch ( rgar( $feed['meta'], 'transactionType' ) ) { case 'subscription' : return esc_html__( 'Subscription', 'gravityforms' ); break; case 'product' : return esc_html__( 'Products and Services', 'gravityforms' ); break; case 'donation' : return esc_html__( 'Donations', 'gravityforms' ); break; } return esc_html__( 'Unsupported transaction type', 'gravityforms' ); } public function get_column_value_amount( $feed ) { $form = $this->get_current_form(); $field_id = $this->get_payment_field( $feed ); if ( $field_id == 'form_total' ) { $label = esc_html__( 'Form Total', 'gravityforms' ); } else { $field = GFFormsModel::get_field( $form, $field_id ); $label = GFCommon::get_label( $field ); } return $label; } //--------- Feed Settings ---------------- /** * Remove the add new button from the title if the form requires a credit card field. * * @return string */ public function feed_list_title() { if ( $this->_requires_credit_card && ! $this->has_credit_card_field( $this->get_current_form() ) ) { return $this->form_settings_title(); } return parent::feed_list_title(); } public function feed_list_message() { if ( $this->_requires_credit_card && ! $this->has_credit_card_field( $this->get_current_form() ) ) { return $this->requires_credit_card_message(); } return parent::feed_list_message(); } public function requires_credit_card_message() { $url = add_query_arg( array( 'view' => null, 'subview' => null ) ); return sprintf( esc_html__( "You must add a Credit Card field to your form before creating a feed. Let's go %sadd one%s!", 'gravityforms' ), "", '' ); } public function feed_settings_fields() { return array( array( 'description' => '', 'fields' => array( array( 'name' => 'feedName', 'label' => esc_html__( 'Name', 'gravityforms' ), 'type' => 'text', 'class' => 'medium', 'required' => true, 'tooltip' => '
" . esc_html__( 'Today', 'gravityforms' ) . " | " . esc_html__( 'Yesterday', 'gravityforms' ) . " | " . esc_html__( 'Last 30 Days', 'gravityforms' ) . " | " . esc_html__( 'Total', 'gravityforms' ) . " |
{$data['summary']['today']['revenue']}
{$data['summary']['today']['subscriptions']} " . esc_html__( 'subscriptions', 'gravityforms' ) . "
{$data['summary']['today']['orders']} " . esc_html__( 'orders', 'gravityforms' ) . "
|
{$data['summary']['yesterday']['revenue']}
{$data['summary']['yesterday']['subscriptions']} " . esc_html__( 'subscriptions', 'gravityforms' ) . "
{$data['summary']['yesterday']['orders']} " . esc_html__( 'orders', 'gravityforms' ) . "
|
{$data['summary']['last30']['revenue']}
{$data['summary']['last30']['subscriptions']} " . esc_html__( 'subscriptions', 'gravityforms' ) . "
{$data['summary']['last30']['orders']} " . esc_html__( 'orders', 'gravityforms' ) . "
|
{$data['summary']['total']['revenue']}
{$data['summary']['total']['subscriptions']} " . esc_html__( 'subscriptions', 'gravityforms' ) . "
{$data['summary']['total']['orders']} " . esc_html__( 'orders', 'gravityforms' ) . '
|