diff --git a/class-gf-paystack.php b/class-gf-paystack.php index f0b2ab2..0627d9b 100644 --- a/class-gf-paystack.php +++ b/class-gf-paystack.php @@ -16,699 +16,699 @@ */ class GFPaystack extends GFPaymentAddOn { - /** - * Contains an instance of this class, if available. - * - * @since 1.0 - * @access private - * - * @used-by GFPaystack::get_instance() - * - * @var object $_instance If available, contains an instance of this class. - */ - private static $_instance = null; - - /** - * Defines the version of the Paystack Add-On. - * - * @since 1.0 - * @access protected - * - * @used-by GFPaystack::scripts() - * - * @var string $_version Contains the version, defined from paystack.php - */ - protected $_version = GF_PAYSTACK_VERSION; - - /** - * Defines the minimum Gravity Forms version required. - * - * @since 1.0 - * @access protected - * - * @var string $_min_gravityforms_version The minimum version required. - */ - protected $_min_gravityforms_version = '2.0'; - - /** - * Defines the plugin slug. - * - * @since 1.0 - * @access protected - * - * @var string $_slug The slug used for this plugin. - */ - protected $_slug = 'gravityformspaystack'; - - /** - * Defines the main plugin file. - * - * @since 1.0 - * @access protected - * - * @var string $_path The path to the main plugin file, relative to the plugins folder. - */ - protected $_path = 'gravityformspaystack/paystack.php'; - - /** - * Defines the full path to this class file. - * - * @since 1.0 - * @access protected - * - * @var string $_full_path The full path. - */ - protected $_full_path = __FILE__; - - /** - * Defines the URL where this Add-On can be found. - * - * @since 1.0 - * @access protected - * - * @var string $_url The URL of the Add-On. - */ - protected $_url = 'https://paystack.com/docs/libraries-and-plugins/plugins#wordpress'; - - /** - * Defines the title of this Add-On. - * - * @since 1.0 - * @access protected - * - * @var string $_title The title of the Add-On. - */ - protected $_title = 'Paystack Add-On for Gravity Forms'; - - /** - * Defines the short title of the Add-On. - * - * @since 1.0 - * @access protected - * - * @var string $_short_title The short title. - */ - protected $_short_title = 'Paystack'; - - /** - * Defines if user will not be able to create feeds for a form until a credit card field has been added. - * - * @since 1.0 - * @access protected - * - * @var bool $_requires_credit_card false. - */ - protected $_requires_credit_card = false; - - /** - * Defines if callbacks/webhooks/IPN will be enabled and the appropriate database table will be created. - * - * @since 1.0 - * @access protected - * - * @var bool $_supports_callbacks true - */ - protected $_supports_callbacks = true; - - /** - * Paystack requires monetary amounts to be formatted as the smallest unit for the currency being used e.g. kobo, pesewas, cent - * - * @since 1.10.1 - * @access protected - * - * @var bool $_requires_smallest_unit true - */ - protected $_requires_smallest_unit = true; - - /** - * Defines the capability needed to access the Add-On settings page. - * - * @since 1.0 - * @access protected - * @var string $_capabilities_settings_page The capability needed to access the Add-On settings page. - */ - protected $_capabilities_settings_page = 'gravityforms_paystack'; - - /** - * Defines the capability needed to access the Add-On form settings page. - * - * @since 1.0 - * @access protected - * @var string $_capabilities_form_settings The capability needed to access the Add-On form settings page. - */ - protected $_capabilities_form_settings = 'gravityforms_paystack'; - - /** - * Defines the capability needed to uninstall the Add-On. - * - * @since 1.0 - * @access protected - * @var string $_capabilities_uninstall The capability needed to uninstall the Add-On. - */ - protected $_capabilities_uninstall = 'gravityforms_paystack_uninstall'; - - /** - * Defines the capabilities needed for the Paystack Add-On - * - * @since 1.0 - * @access protected - * @var array $_capabilities The capabilities needed for the Add-On - */ - protected $_capabilities = array('gravityforms_paystack', 'gravityforms_paystack_uninstall'); - - /** - * Holds the custom meta key currently being processed. Enables the key to be passed to the gform_paystack_field_value filter. - * - * @since 1.0 - * @access protected - * - * @used-by GFPaystack::maybe_override_field_value() - * - * @var string $_current_meta_key The meta key currently being processed. - */ - protected $_current_meta_key = ''; - - /** - * Enable rate limits to log card errors etc. - * - * @since 1.0 - * - * @var bool - */ - protected $_enable_rate_limits = true; - - /** - * Paystack API Wrapper - * - * @var GFPaystackApi - */ - protected $paystack_api; - - /** - * Get an instance of this class. - * - * @since 1.0 - * @access public - * - * @uses GFPaystack - * @uses GFPaystack::$_instance - * - * @return object GFPaystack - */ - public static function get_instance() - { - if (null === self::$_instance) { - self::$_instance = new GFPaystack(); - } - - return self::$_instance; - } - - /** - * Load the Paystack credit card field. - * - * @since 1.0 - */ - public function pre_init() - { - // For form confirmation redirection, this must be called in `wp`, - // or confirmation redirect to a page would throw PHP fatal error. - // Run before calling parent method. We don't want to run anything else before displaying thank you page. - add_action('wp', array($this, 'maybe_thankyou_page'), 5); - - parent::pre_init(); - } - - public function init() - { - parent::init(); - - add_filter('gform_currencies', array($this, 'add_paystack_currencies')); - } - - // # PLUGIN SETTINGS ----------------------------------------------------------------------------------------------- - - /** - * Configures the settings which should be rendered on the add-on settings tab. - * - * @access public - * - * @used-by GFAddOn::maybe_save_plugin_settings() - * @used-by GFAddOn::plugin_settings_page() - * @uses GFPaystack::api_settings_fields() - * @uses GFPaystack::get_webhooks_section_description() - * - * @return array Plugin settings fields to add. - */ - public function plugin_settings_fields() - { - $fields = array( - array( - 'title' => esc_html__('Configuration', 'gravityformspaystack'), - 'fields' => $this->api_settings_fields(), - ), - ); - - return $fields; - } - - /** - * Define the settings which appear in the Paystack API section. - * - * @access public - * - * @used-by GFPaystack::plugin_settings_fields() - * - * @return array The API settings fields. - */ - public function api_settings_fields() - { - $api_mode = ''; - - if ($this->is_detail_page() && empty($api_mode)) { - $api_mode = $this->get_plugin_setting('api_mode'); - } - - $fields = array( - array( - 'name' => 'api_mode', - 'label' => esc_html__('Mode', 'gravityformspaystack'), - 'type' => 'radio', - 'default_value' => $api_mode, - 'choices' => array( - array( - 'label' => esc_html__('Live', 'gravityformspaystack'), - 'value' => 'live', - ), - array( - 'label' => esc_html__('Test', 'gravityformspaystack'), - 'value' => 'test', - ), - ), - 'horizontal' => true, - ) - ); - - $credentials_fields = array( - array( - 'name' => 'test_public_key', - 'label' => esc_html__('Test Public Key', 'gravityformspaystack'), - 'type' => 'text', - 'class' => 'medium', - ), - array( - 'name' => 'test_secret_key', - 'label' => esc_html__('Test Secret Key', 'gravityformspaystack'), - 'type' => 'text', - 'class' => 'medium', - ), - array( - 'name' => 'live_public_key', - 'label' => esc_html__('Live Public Key', 'gravityformspaystack'), - 'type' => 'text', - 'class' => 'medium', - ), - array( - 'name' => 'live_secret_key', - 'label' => esc_html__('Live Secret Key', 'gravityformspaystack'), - 'type' => 'text', - 'class' => 'medium', - ), - ); - - $fields = array_merge($fields, $credentials_fields); - - $webhook_fields = array( - array( - 'name' => 'webhooks_enabled', - 'label' => esc_html__('Webhooks Enabled?', 'gravityformspaystack'), - 'type' => 'checkbox', - 'horizontal' => true, - 'required' => ($this->get_current_feed_id() || !isset($_GET['fid'])) ? true : false, - 'description' => $this->get_webhooks_section_description(), - 'dependency' => $this->is_detail_page() ? array($this, 'is_feed_paystack_connect_enabled') : false, - 'choices' => array( - array( - 'label' => esc_html__('I have enabled the Gravity Forms webhook URL in my Paystack account.', 'gravityformspaystack'), - 'value' => 1, - 'name' => 'webhooks_enabled', - ), - ), - ) - ); - - $fields = array_merge($fields, $webhook_fields); - - return $fields; - } - - /** - * Define the markup to be displayed for the webhooks section description. - * - * @since Unknown - * @access public - * - * @used-by GFPaystack::plugin_settings_fields() - * @uses GFPaystack::get_webhook_url() - * - * @return string HTML formatted webhooks description. - */ - public function get_webhooks_section_description() - { - ob_start(); -?> - - - -

- - - - 'mode', - 'label' => esc_html__('Mode', 'gravityformspaystack'), - 'type' => 'radio', - 'choices' => array( - array('id' => 'gf_paystack_live_mode', 'label' => esc_html__('Live', 'gravityformspaystack'), 'value' => 'live'), - array('id' => 'gf_paystack_test_mode', 'label' => esc_html__('Test', 'gravityformspaystack'), 'value' => 'test'), - ), - 'horizontal' => true, - 'default_value' => 'live', - 'tooltip' => '
' . esc_html__('Mode', 'gravityformspaystack') . '
' . esc_html__('Select Live to receive payments in production. Select Test for testing purposes when using the Paystack development sandbox.', 'gravityformspaystack') - ), - ); - - $default_settings = parent::add_field_after('feedName', $fields, $default_settings); - - // Add donation to transaction type drop down - // $transaction_type = parent::get_field('transactionType', $default_settings); - // $choices = $transaction_type['choices']; - // $add_donation = true; - - // foreach ($choices as $choice) { - // // Add donation option if it does not already exist - // if ($choice['value'] == 'donation') { - // $add_donation = false; - // } - // } - - // if ($add_donation) { - // // Add donation transaction type - // $choices[] = array('label' => __('Donations', 'gravityformspaystack'), 'value' => 'donation'); - // } - - // $transaction_type['choices'] = $choices; - // $default_settings = $this->replace_field('transactionType', $transaction_type, $default_settings); - - // Add send invoice field if the feed transaction type is a subscription. - if ($this->get_setting('transactionType') === 'subscription') { - $invoice_settings = array( - 'name' => 'sendInvoices', - 'label' => esc_html__('Send Invoices', 'gravityformspaystack'), - 'type' => 'checkbox', - 'choices' => array( - array( - 'label' => 'Check if you want invoices to be sent to your customers', - 'name' => 'sendInvoices', - ), - ), - 'tooltip' => '
' . esc_html__('Send Invoices', 'gravityformspaystack') . '
' . esc_html__('If enabled, Paystack can send an email invoice to customers.', 'gravityformspaystack'), - ); - - $default_settings = $this->add_field_before('setupFee', $invoice_settings, $default_settings); - } - - // hide default display of setup fee, not used by Paystack - $default_settings = parent::remove_field('setupFee', $default_settings); - - // // Prepare trial period field. - // $trial_period_field = array( - // 'name' => 'trialPeriod', - // 'label' => esc_html__('Trial Period', 'gravityformspaystack'), - // 'style' => 'width:40px;text-align:center;', - // 'type' => 'trial_period', - // 'after_input' => ' ' . esc_html__('days', 'gravityformspaystack'), - // 'validation_callback' => array($this, 'validate_trial_period'), - // ); - - // // Add trial period field. - // $default_settings = $this->add_field_after('trial', $trial_period_field, $default_settings); - - // Hide default display of trial, not used by Paystack - $default_settings = parent::remove_field('trial', $default_settings); - - // Add subscription name field. - $plan_name_field = array( - 'name' => 'planName', - 'label' => esc_html__('Plan Name', 'gravityformspaystack'), - 'type' => 'text', - 'class' => 'medium merge-tag-support mt-hide_all_fields mt-position-right', - 'tooltip' => '
' . esc_html__('Plan Name', 'gravityformspaystack') . '
' . esc_html__('Enter a name for the subscription. It will be displayed on the payment form as well as the Paystack dashboard.', 'gravityformspaystack'), - ); - - $default_settings = $this->add_field_before('recurringAmount', $plan_name_field, $default_settings); - - // Customer information fields. - $customer_info_field = array( - 'name' => 'customerInformation', - 'label' => esc_html__('Customer Information', 'gravityformspaystack'), - 'type' => 'field_map', - 'field_map' => array( - array( - 'name' => 'email', - 'label' => esc_html__('Email Address', 'gravityformspaystack'), - 'required' => true, - 'field_type' => array('email', 'hidden'), - 'tooltip' => '
' . esc_html__('Email', 'gravityformspaystack') . '
' . esc_html__('You can specify an email field and it will be sent to the Paystack screen as the customer\'s email.', 'gravityformspaystack'), - ), - array( - 'name' => 'first_name', - 'label' => esc_html__('First Name', 'gravityformspaystack'), - 'required' => ($this->get_setting('transactionType') == 'subscription') ? true : false, - ), - array( - 'name' => 'last_name', - 'label' => esc_html__('Last Name', 'gravityformspaystack'), - 'required' => ($this->get_setting('transactionType') == 'subscription') ? true : false, - ), - array( - 'name' => 'phone', - 'label' => esc_html__('Phone Number', 'gravityformspaystack'), - 'required' => false, - ), - ), - ); - - if ($this->get_setting('transactionType') == 'subscription') { - $customer_info_field['field_map'][] = array( - 'name' => 'recurring_times', - 'label' => esc_html__('Recurring Times', 'gravityformspaystack'), - 'required' => false, - 'tooltip' => '
' . esc_html__('Recurring Times', 'gravityformspaystack') . '
' . esc_html__('(OPTIONAL). Setting this will allow users to select then number of times a charge should happen during a subscrption to a plan. This will override the "Recurring Times" option under "Subscription Settings" section above. It is recommended to create a select field. Values can be "2-100" or "0" for indefinite. In case an empty value or 1 is passed, the plugin will fall back to the other aforementioned "Recurring Times" option.', 'gravityformspaystack'), - ); - } - - // Add customer information fields. - $default_settings = $this->add_field_before('billingInformation', $customer_info_field, $default_settings); - - // Prepare meta data field. - $custom_meta = array( - array( - 'name' => 'metaData', - 'label' => esc_html__('Metadata', 'gravityformspaystack'), - 'type' => 'dynamic_field_map', - 'limit' => 20, - 'tooltip' => '
' . esc_html__('Metadata', 'gravityformspaystack') . '
' . esc_html__('You may send custom meta information to Paystack. A maximum of 20 custom keys may be sent.', 'gravityformspaystack'), - 'validation_callback' => array($this, 'validate_custom_meta'), - ), - ); - - // Add meta data field. - $default_settings = $this->add_field_after('billingInformation', $custom_meta, $default_settings); - - // hide default display of billing Information if transaction type is donation - if ($this->get_setting('transactionType') === 'donation') { - $default_settings = parent::remove_field('billingInformation', $default_settings); - } - - return $default_settings; - } - - /** - * Define the markup for the billing_cycle type field. - * - * @access public - * - * @return array The feed settings. - */ - public function settings_billing_cycle($field, $echo = true) - { - $intervals = $this->supported_billing_intervals(); - - $choices = array(); - foreach ($intervals as $unit => $interval) { - if (!empty($interval)) { - $choices[] = array('value' => $unit, 'label' => $interval['label']); - } - } - - //Interval drop down - $interval_field = array( - 'name' => $field['name'] . '_unit', - 'type' => 'select', - 'onchange' => "loadBillingLength('" . esc_attr($field['name']) . "')", - 'choices' => $choices, - ); - - $html = ' ' . $this->settings_select($interval_field, false); - - $html .= "'; - - if ($echo) { - echo $html; - } - - return $html; - } - - /** - * Define the choices available in the billing cycle dropdowns. - * - * @access public - * - * @used-by GFPaymentAddOn::settings_billing_cycle() - * - * @return array Billing intervals that are supported. - */ - public function supported_billing_intervals() - { - return array( - 'hourly' => array('label' => esc_html__('Hourly', 'gravityformspaystack')), - 'daily' => array('label' => esc_html__('Daily', 'gravityformspaystack')), - 'weekly' => array('label' => esc_html__('Weekly', 'gravityformspaystack')), - 'monthly' => array('label' => esc_html__('Monthly', 'gravityformspaystack')), - 'annually' => array('label' => esc_html__('Annually', 'gravityformspaystack')), - 'biannually' => array('label' => esc_html__('Biannually', 'gravityformspaystack')), - ); - } - - /** - * Define the markup for the trial type field. - * - * @since Unknown - * @access public - * - * @uses GFAddOn::settings_checkbox() - * - * @param array $field The field properties. - * @param bool|true $echo Should the setting markup be echoed. Defaults to true. - * - * @return string|void The HTML markup if $echo is set to false. Void otherwise. - */ - public function settings_trial($field, $echo = true) - { - // Prepare enabled field settings. - $enabled_field = array( - 'name' => $field['name'] . '_checkbox', - 'type' => 'checkbox', - 'horizontal' => true, - 'choices' => array( - array( - 'label' => esc_html__('Enabled', 'gravityformspaystack'), - 'name' => $field['name'] . '_enabled', - 'value' => '1', - 'onchange' => "if(jQuery(this).prop('checked')){ + /** + * Contains an instance of this class, if available. + * + * @since 1.0 + * @access private + * + * @used-by GFPaystack::get_instance() + * + * @var object $_instance If available, contains an instance of this class. + */ + private static $_instance = null; + + /** + * Defines the version of the Paystack Add-On. + * + * @since 1.0 + * @access protected + * + * @used-by GFPaystack::scripts() + * + * @var string $_version Contains the version, defined from paystack.php + */ + protected $_version = GF_PAYSTACK_VERSION; + + /** + * Defines the minimum Gravity Forms version required. + * + * @since 1.0 + * @access protected + * + * @var string $_min_gravityforms_version The minimum version required. + */ + protected $_min_gravityforms_version = '2.0'; + + /** + * Defines the plugin slug. + * + * @since 1.0 + * @access protected + * + * @var string $_slug The slug used for this plugin. + */ + protected $_slug = 'gravityformspaystack'; + + /** + * Defines the main plugin file. + * + * @since 1.0 + * @access protected + * + * @var string $_path The path to the main plugin file, relative to the plugins folder. + */ + protected $_path = 'gravityformspaystack/paystack.php'; + + /** + * Defines the full path to this class file. + * + * @since 1.0 + * @access protected + * + * @var string $_full_path The full path. + */ + protected $_full_path = __FILE__; + + /** + * Defines the URL where this Add-On can be found. + * + * @since 1.0 + * @access protected + * + * @var string $_url The URL of the Add-On. + */ + protected $_url = 'https://paystack.com/docs/libraries-and-plugins/plugins#wordpress'; + + /** + * Defines the title of this Add-On. + * + * @since 1.0 + * @access protected + * + * @var string $_title The title of the Add-On. + */ + protected $_title = 'Paystack Add-On for Gravity Forms'; + + /** + * Defines the short title of the Add-On. + * + * @since 1.0 + * @access protected + * + * @var string $_short_title The short title. + */ + protected $_short_title = 'Paystack'; + + /** + * Defines if user will not be able to create feeds for a form until a credit card field has been added. + * + * @since 1.0 + * @access protected + * + * @var bool $_requires_credit_card false. + */ + protected $_requires_credit_card = false; + + /** + * Defines if callbacks/webhooks/IPN will be enabled and the appropriate database table will be created. + * + * @since 1.0 + * @access protected + * + * @var bool $_supports_callbacks true + */ + protected $_supports_callbacks = true; + + /** + * Paystack requires monetary amounts to be formatted as the smallest unit for the currency being used e.g. kobo, pesewas, cent + * + * @since 1.10.1 + * @access protected + * + * @var bool $_requires_smallest_unit true + */ + protected $_requires_smallest_unit = true; + + /** + * Defines the capability needed to access the Add-On settings page. + * + * @since 1.0 + * @access protected + * @var string $_capabilities_settings_page The capability needed to access the Add-On settings page. + */ + protected $_capabilities_settings_page = 'gravityforms_paystack'; + + /** + * Defines the capability needed to access the Add-On form settings page. + * + * @since 1.0 + * @access protected + * @var string $_capabilities_form_settings The capability needed to access the Add-On form settings page. + */ + protected $_capabilities_form_settings = 'gravityforms_paystack'; + + /** + * Defines the capability needed to uninstall the Add-On. + * + * @since 1.0 + * @access protected + * @var string $_capabilities_uninstall The capability needed to uninstall the Add-On. + */ + protected $_capabilities_uninstall = 'gravityforms_paystack_uninstall'; + + /** + * Defines the capabilities needed for the Paystack Add-On + * + * @since 1.0 + * @access protected + * @var array $_capabilities The capabilities needed for the Add-On + */ + protected $_capabilities = array('gravityforms_paystack', 'gravityforms_paystack_uninstall'); + + /** + * Holds the custom meta key currently being processed. Enables the key to be passed to the gform_paystack_field_value filter. + * + * @since 1.0 + * @access protected + * + * @used-by GFPaystack::maybe_override_field_value() + * + * @var string $_current_meta_key The meta key currently being processed. + */ + protected $_current_meta_key = ''; + + /** + * Enable rate limits to log card errors etc. + * + * @since 1.0 + * + * @var bool + */ + protected $_enable_rate_limits = true; + + /** + * Paystack API Wrapper + * + * @var GFPaystackApi + */ + protected $paystack_api; + + /** + * Get an instance of this class. + * + * @since 1.0 + * @access public + * + * @uses GFPaystack + * @uses GFPaystack::$_instance + * + * @return object GFPaystack + */ + public static function get_instance() + { + if (null === self::$_instance) { + self::$_instance = new GFPaystack(); + } + + return self::$_instance; + } + + /** + * Load the Paystack credit card field. + * + * @since 1.0 + */ + public function pre_init() + { + // For form confirmation redirection, this must be called in `wp`, + // or confirmation redirect to a page would throw PHP fatal error. + // Run before calling parent method. We don't want to run anything else before displaying thank you page. + add_action('wp', array($this, 'maybe_thankyou_page'), 5); + + parent::pre_init(); + } + + public function init() + { + parent::init(); + + add_filter('gform_currencies', array($this, 'add_paystack_currencies')); + } + + // # PLUGIN SETTINGS ----------------------------------------------------------------------------------------------- + + /** + * Configures the settings which should be rendered on the add-on settings tab. + * + * @access public + * + * @used-by GFAddOn::maybe_save_plugin_settings() + * @used-by GFAddOn::plugin_settings_page() + * @uses GFPaystack::api_settings_fields() + * @uses GFPaystack::get_webhooks_section_description() + * + * @return array Plugin settings fields to add. + */ + public function plugin_settings_fields() + { + $fields = array( + array( + 'title' => esc_html__('Configuration', 'gravityformspaystack'), + 'fields' => $this->api_settings_fields(), + ), + ); + + return $fields; + } + + /** + * Define the settings which appear in the Paystack API section. + * + * @access public + * + * @used-by GFPaystack::plugin_settings_fields() + * + * @return array The API settings fields. + */ + public function api_settings_fields() + { + $api_mode = ''; + + if ($this->is_detail_page() && empty($api_mode)) { + $api_mode = $this->get_plugin_setting('api_mode'); + } + + $fields = array( + array( + 'name' => 'api_mode', + 'label' => esc_html__('Mode', 'gravityformspaystack'), + 'type' => 'radio', + 'default_value' => $api_mode, + 'choices' => array( + array( + 'label' => esc_html__('Live', 'gravityformspaystack'), + 'value' => 'live', + ), + array( + 'label' => esc_html__('Test', 'gravityformspaystack'), + 'value' => 'test', + ), + ), + 'horizontal' => true, + ) + ); + + $credentials_fields = array( + array( + 'name' => 'test_public_key', + 'label' => esc_html__('Test Public Key', 'gravityformspaystack'), + 'type' => 'text', + 'class' => 'medium', + ), + array( + 'name' => 'test_secret_key', + 'label' => esc_html__('Test Secret Key', 'gravityformspaystack'), + 'type' => 'text', + 'class' => 'medium', + ), + array( + 'name' => 'live_public_key', + 'label' => esc_html__('Live Public Key', 'gravityformspaystack'), + 'type' => 'text', + 'class' => 'medium', + ), + array( + 'name' => 'live_secret_key', + 'label' => esc_html__('Live Secret Key', 'gravityformspaystack'), + 'type' => 'text', + 'class' => 'medium', + ), + ); + + $fields = array_merge($fields, $credentials_fields); + + $webhook_fields = array( + array( + 'name' => 'webhooks_enabled', + 'label' => esc_html__('Webhooks Enabled?', 'gravityformspaystack'), + 'type' => 'checkbox', + 'horizontal' => true, + 'required' => ($this->get_current_feed_id() || !isset($_GET['fid'])) ? true : false, + 'description' => $this->get_webhooks_section_description(), + 'dependency' => $this->is_detail_page() ? array($this, 'is_feed_paystack_connect_enabled') : false, + 'choices' => array( + array( + 'label' => esc_html__('I have enabled the Gravity Forms webhook URL in my Paystack account.', 'gravityformspaystack'), + 'value' => 1, + 'name' => 'webhooks_enabled', + ), + ), + ) + ); + + $fields = array_merge($fields, $webhook_fields); + + return $fields; + } + + /** + * Define the markup to be displayed for the webhooks section description. + * + * @since Unknown + * @access public + * + * @used-by GFPaystack::plugin_settings_fields() + * @uses GFPaystack::get_webhook_url() + * + * @return string HTML formatted webhooks description. + */ + public function get_webhooks_section_description() + { + ob_start(); + ?> + + + +

+ + + + 'mode', + 'label' => esc_html__('Mode', 'gravityformspaystack'), + 'type' => 'radio', + 'choices' => array( + array('id' => 'gf_paystack_live_mode', 'label' => esc_html__('Live', 'gravityformspaystack'), 'value' => 'live'), + array('id' => 'gf_paystack_test_mode', 'label' => esc_html__('Test', 'gravityformspaystack'), 'value' => 'test'), + ), + 'horizontal' => true, + 'default_value' => 'live', + 'tooltip' => '
' . esc_html__('Mode', 'gravityformspaystack') . '
' . esc_html__('Select Live to receive payments in production. Select Test for testing purposes when using the Paystack development sandbox.', 'gravityformspaystack') + ), + ); + + $default_settings = parent::add_field_after('feedName', $fields, $default_settings); + + // Add donation to transaction type drop down + // $transaction_type = parent::get_field('transactionType', $default_settings); + // $choices = $transaction_type['choices']; + // $add_donation = true; + + // foreach ($choices as $choice) { + // // Add donation option if it does not already exist + // if ($choice['value'] == 'donation') { + // $add_donation = false; + // } + // } + + // if ($add_donation) { + // // Add donation transaction type + // $choices[] = array('label' => __('Donations', 'gravityformspaystack'), 'value' => 'donation'); + // } + + // $transaction_type['choices'] = $choices; + // $default_settings = $this->replace_field('transactionType', $transaction_type, $default_settings); + + // Add send invoice field if the feed transaction type is a subscription. + if ($this->get_setting('transactionType') === 'subscription') { + $invoice_settings = array( + 'name' => 'sendInvoices', + 'label' => esc_html__('Send Invoices', 'gravityformspaystack'), + 'type' => 'checkbox', + 'choices' => array( + array( + 'label' => 'Check if you want invoices to be sent to your customers', + 'name' => 'sendInvoices', + ), + ), + 'tooltip' => '
' . esc_html__('Send Invoices', 'gravityformspaystack') . '
' . esc_html__('If enabled, Paystack can send an email invoice to customers.', 'gravityformspaystack'), + ); + + $default_settings = $this->add_field_before('setupFee', $invoice_settings, $default_settings); + } + + // hide default display of setup fee, not used by Paystack + $default_settings = parent::remove_field('setupFee', $default_settings); + + // // Prepare trial period field. + // $trial_period_field = array( + // 'name' => 'trialPeriod', + // 'label' => esc_html__('Trial Period', 'gravityformspaystack'), + // 'style' => 'width:40px;text-align:center;', + // 'type' => 'trial_period', + // 'after_input' => ' ' . esc_html__('days', 'gravityformspaystack'), + // 'validation_callback' => array($this, 'validate_trial_period'), + // ); + + // // Add trial period field. + // $default_settings = $this->add_field_after('trial', $trial_period_field, $default_settings); + + // Hide default display of trial, not used by Paystack + $default_settings = parent::remove_field('trial', $default_settings); + + // Add subscription name field. + $plan_name_field = array( + 'name' => 'planName', + 'label' => esc_html__('Plan Name', 'gravityformspaystack'), + 'type' => 'text', + 'class' => 'medium merge-tag-support mt-hide_all_fields mt-position-right', + 'tooltip' => '
' . esc_html__('Plan Name', 'gravityformspaystack') . '
' . esc_html__('Enter a name for the subscription. It will be displayed on the payment form as well as the Paystack dashboard.', 'gravityformspaystack'), + ); + + $default_settings = $this->add_field_before('recurringAmount', $plan_name_field, $default_settings); + + // Customer information fields. + $customer_info_field = array( + 'name' => 'customerInformation', + 'label' => esc_html__('Customer Information', 'gravityformspaystack'), + 'type' => 'field_map', + 'field_map' => array( + array( + 'name' => 'email', + 'label' => esc_html__('Email Address', 'gravityformspaystack'), + 'required' => true, + 'field_type' => array('email', 'hidden'), + 'tooltip' => '
' . esc_html__('Email', 'gravityformspaystack') . '
' . esc_html__('You can specify an email field and it will be sent to the Paystack screen as the customer\'s email.', 'gravityformspaystack'), + ), + array( + 'name' => 'first_name', + 'label' => esc_html__('First Name', 'gravityformspaystack'), + 'required' => ($this->get_setting('transactionType') == 'subscription') ? true : false, + ), + array( + 'name' => 'last_name', + 'label' => esc_html__('Last Name', 'gravityformspaystack'), + 'required' => ($this->get_setting('transactionType') == 'subscription') ? true : false, + ), + array( + 'name' => 'phone', + 'label' => esc_html__('Phone Number', 'gravityformspaystack'), + 'required' => false, + ), + ), + ); + + if ($this->get_setting('transactionType') == 'subscription') { + $customer_info_field['field_map'][] = array( + 'name' => 'recurring_times', + 'label' => esc_html__('Recurring Times', 'gravityformspaystack'), + 'required' => false, + 'tooltip' => '
' . esc_html__('Recurring Times', 'gravityformspaystack') . '
' . esc_html__('(OPTIONAL). Setting this will allow users to select then number of times a charge should happen during a subscrption to a plan. This will override the "Recurring Times" option under "Subscription Settings" section above. It is recommended to create a select field. Values can be "2-100" or "0" for indefinite. In case an empty value or 1 is passed, the plugin will fall back to the other aforementioned "Recurring Times" option.', 'gravityformspaystack'), + ); + } + + // Add customer information fields. + $default_settings = $this->add_field_before('billingInformation', $customer_info_field, $default_settings); + + // Prepare meta data field. + $custom_meta = array( + array( + 'name' => 'metaData', + 'label' => esc_html__('Metadata', 'gravityformspaystack'), + 'type' => 'dynamic_field_map', + 'limit' => 20, + 'tooltip' => '
' . esc_html__('Metadata', 'gravityformspaystack') . '
' . esc_html__('You may send custom meta information to Paystack. A maximum of 20 custom keys may be sent.', 'gravityformspaystack'), + 'validation_callback' => array($this, 'validate_custom_meta'), + ), + ); + + // Add meta data field. + $default_settings = $this->add_field_after('billingInformation', $custom_meta, $default_settings); + + // hide default display of billing Information if transaction type is donation + if ($this->get_setting('transactionType') === 'donation') { + $default_settings = parent::remove_field('billingInformation', $default_settings); + } + + return $default_settings; + } + + /** + * Define the markup for the billing_cycle type field. + * + * @access public + * + * @return array The feed settings. + */ + public function settings_billing_cycle($field, $echo = true) + { + $intervals = $this->supported_billing_intervals(); + + $choices = array(); + foreach ($intervals as $unit => $interval) { + if (!empty($interval)) { + $choices[] = array('value' => $unit, 'label' => $interval['label']); + } + } + + //Interval drop down + $interval_field = array( + 'name' => $field['name'] . '_unit', + 'type' => 'select', + 'onchange' => "loadBillingLength('" . esc_attr($field['name']) . "')", + 'choices' => $choices, + ); + + $html = ' ' . $this->settings_select($interval_field, false); + + $html .= "'; + + if ($echo) { + echo $html; + } + + return $html; + } + + /** + * Define the choices available in the billing cycle dropdowns. + * + * @access public + * + * @used-by GFPaymentAddOn::settings_billing_cycle() + * + * @return array Billing intervals that are supported. + */ + public function supported_billing_intervals() + { + return array( + 'hourly' => array('label' => esc_html__('Hourly', 'gravityformspaystack')), + 'daily' => array('label' => esc_html__('Daily', 'gravityformspaystack')), + 'weekly' => array('label' => esc_html__('Weekly', 'gravityformspaystack')), + 'monthly' => array('label' => esc_html__('Monthly', 'gravityformspaystack')), + 'annually' => array('label' => esc_html__('Annually', 'gravityformspaystack')), + 'biannually' => array('label' => esc_html__('Biannually', 'gravityformspaystack')), + ); + } + + /** + * Define the markup for the trial type field. + * + * @since Unknown + * @access public + * + * @uses GFAddOn::settings_checkbox() + * + * @param array $field The field properties. + * @param bool|true $echo Should the setting markup be echoed. Defaults to true. + * + * @return string|void The HTML markup if $echo is set to false. Void otherwise. + */ + public function settings_trial($field, $echo = true) + { + // Prepare enabled field settings. + $enabled_field = array( + 'name' => $field['name'] . '_checkbox', + 'type' => 'checkbox', + 'horizontal' => true, + 'choices' => array( + array( + 'label' => esc_html__('Enabled', 'gravityformspaystack'), + 'name' => $field['name'] . '_enabled', + 'value' => '1', + 'onchange' => "if(jQuery(this).prop('checked')){ jQuery('#gaddon-setting-row-trialPeriod').show('slow'); } else { jQuery('#gaddon-setting-row-trialPeriod').hide('slow'); jQuery('#trialPeriod').val( '' ); }", - ), - ), - ); - - // Get checkbox markup. - $html = $this->settings_checkbox($enabled_field, false); - - // Echo setting markup, if enabled. - if ($echo) { - echo $html; - } - - return $html; - } - - /** - * Define the markup for the trial_period type field. - * - * @since Unknown - * @access public - * - * @uses GFAddOn::settings_text() - * @uses GFAddOn::field_failed_validation() - * @uses GFAddOn::get_error_icon() - * - * @param array $field The field properties. - * @param bool|true $echo Should the setting markup be echoed. Defaults to true. - * - * @return string|void The HTML markup if $echo is set to false. Void otherwise. - */ - public function settings_trial_period($field, $echo = true) - { - // Get text input markup. - $html = $this->settings_text($field, false); - - // Prepare validation placeholder name. - $validation_placeholder = array('name' => 'trialValidationPlaceholder'); - - // Add validation indicator. - if ($this->field_failed_validation($validation_placeholder)) { - $html .= ' ' . $this->get_error_icon($validation_placeholder); - } - - // If trial is not enabled and setup fee is enabled, hide field. - $html .= ' + ), + ), + ); + + // Get checkbox markup. + $html = $this->settings_checkbox($enabled_field, false); + + // Echo setting markup, if enabled. + if ($echo) { + echo $html; + } + + return $html; + } + + /** + * Define the markup for the trial_period type field. + * + * @since Unknown + * @access public + * + * @uses GFAddOn::settings_text() + * @uses GFAddOn::field_failed_validation() + * @uses GFAddOn::get_error_icon() + * + * @param array $field The field properties. + * @param bool|true $echo Should the setting markup be echoed. Defaults to true. + * + * @return string|void The HTML markup if $echo is set to false. Void otherwise. + */ + public function settings_trial_period($field, $echo = true) + { + // Get text input markup. + $html = $this->settings_text($field, false); + + // Prepare validation placeholder name. + $validation_placeholder = array('name' => 'trialValidationPlaceholder'); + + // Add validation indicator. + if ($this->field_failed_validation($validation_placeholder)) { + $html .= ' ' . $this->get_error_icon($validation_placeholder); + } + + // If trial is not enabled and setup fee is enabled, hide field. + $html .= ' '; - // Echo setting markup, if enabled. - if ($echo) { - echo $html; - } - - return $html; - } - - /** - * Prepare fields for field mapping in feed settings. - * - * - * @return array $fields - */ - public function billing_info_fields() - { - $fields = array( - array( - 'name' => 'address_line1', - 'label' => __('Address', 'gravityformsconstantcontact'), - 'required' => false, - 'field_type' => array('address'), - ), - array( - 'name' => 'address_line2', - 'label' => __('Address 2', 'gravityformsconstantcontact'), - 'required' => false, - 'field_type' => array('address'), - ), - array( - 'name' => 'address_city', - 'label' => __('City', 'gravityformsconstantcontact'), - 'required' => false, - 'field_type' => array('address'), - ), - array( - 'name' => 'address_state', - 'label' => __('State', 'gravityformsconstantcontact'), - 'required' => false, - 'field_type' => array('address'), - ), - array( - 'name' => 'address_zip', - 'label' => __('Zip', 'gravityformsconstantcontact'), - 'required' => false, - 'field_type' => array('address'), - ), - array( - 'name' => 'address_country', - 'label' => __('Country', 'gravityformsconstantcontact'), - 'required' => false, - 'field_type' => array('address'), - ), - ); - - return $fields; - } - - /** - * Validate the custom_meta type field. - * - * @access public - * - * @uses GFAddOn::get_posted_settings() - * @uses GFAddOn::set_field_error() - * - * @param array $field The field properties. Not used. - * - * @return void - */ - public function validate_custom_meta($field) - { - /* - * Number of keys is limited to 20. - * Interface should control this, validating just in case. - * Key names have maximum length of 40 characters. - */ - - // Get metadata from posted settings. - $settings = $this->get_posted_settings(); - $meta_data = $settings['metaData']; - - // If metadata is not defined, return. - if (empty($meta_data)) { - return; - } - - // Get number of metadata items. - $meta_count = count($meta_data); - - // If there are more than 20 metadata keys, set field error. - if ($meta_count > 20) { - $this->set_field_error(array(esc_html__('You may only have 20 custom keys.'), 'gravityformspaystack')); - return; - } - - // Loop through metadata and check the key name length (custom_key). - foreach ($meta_data as $meta) { - if (empty($meta['custom_key']) && !empty($meta['value'])) { - $this->set_field_error(array('name' => 'metaData'), esc_html__("A field has been mapped to a custom key without a name. Please enter a name for the custom key, remove the metadata item, or return the corresponding drop down to 'Select a Field'.", 'gravityformspaystack')); - break; - } else if (strlen($meta['custom_key']) > 40) { - $this->set_field_error(array('name' => 'metaData'), sprintf(esc_html__('The name of custom key %s is too long. Please shorten this to 40 characters or less.', 'gravityformspaystack'), $meta['custom_key'])); - break; - } - } - } - - /** - * Prevent the 'options' checkboxes setting being included on the feed. - * - * @access public - * - * @used-by GFPaymentAddOn::other_settings_fields() - * - * @return false - */ - public function option_choices() - { - return false; - } - - /** - * Add supported notification events. - * - * @access public - * - * @used-by GFFeedAddOn::notification_events() - * @uses GFFeedAddOn::has_feed() - * - * @param array $form The form currently being processed. - * - * @return array|false The supported notification events. False if feed cannot be found within $form. - */ - public function supported_notification_events($form) - { - // If this form does not have a Paystack feed, return false. - if (!$this->has_feed($form['id'])) { - return false; - } - - // Return Paystack notification events. - return array( - 'complete_payment' => esc_html__('Payment Completed', 'gravityformspaystack'), - 'refund_payment' => esc_html__('Payment Refunded', 'gravityformspaystack'), - 'fail_payment' => esc_html__('Payment Failed', 'gravityformspaystack'), - 'create_subscription' => esc_html__('Subscription Created', 'gravityformspaystack'), - 'cancel_subscription' => esc_html__('Subscription Canceled', 'gravityformspaystack'), - 'add_subscription_payment' => esc_html__('Subscription Payment Added', 'gravityformspaystack'), - 'fail_subscription_payment' => esc_html__('Subscription Payment Failed', 'gravityformspaystack'), - ); - } - - // # PAYSTACK TRANSACTIONS ------------------------------------------------------------------------------------------- - - /** - * Useful when developing a payment gateway that processes the payment outside of the website (i.e. Paystack Redirect). - * - * @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) - { - // Don't process redirect url if request is a Paystack return - if (!rgempty('gf_paystack_return', $_GET)) { - return false; - } - - // Getting mode (Live (Production) or Test (Sandbox)) - $mode = $feed['meta']['mode']; - - // Setup the Paystack API - $this->paystack_api($mode); - - // Get the transaction type - $transaction_type = $feed['meta']['transactionType']; - - // Getting the product status - $is_product = $feed['meta']['transactionType'] === 'product'; - - // Getting the subscription status - $is_subscription = $feed['meta']['transactionType'] === 'subscription'; - - // URL that will listen to callback from Paystack - $page_url = get_bloginfo('url'); - $ids_query = "ids={$entry['id']}|{$feed['id']}|{$form['id']}"; - $ids_query .= '&hash=' . wp_hash($ids_query); - - $return_url = add_query_arg('gf_paystack_return', base64_encode($ids_query), $page_url); - - // $setup_fee = rgar($submission_data, 'setup_fee'); - // $trial_amount = rgar($submission_data, 'trial'); - $payment_amount = rgar($submission_data, 'payment_amount'); - - // Currency - $currency = rgar($entry, 'currency'); - - // Customer Info - $customer_info = $this->get_fields_meta_data($feed, $entry, $this->get_customer_fields()); - - // Get feed custom metadata - $custom_data = $this->get_paystack_meta_data($feed, $entry, $form); - - // $custom_data[] = [ - // 'display_name' => 'Plugin Name', - // 'variable_name' => 'plugin_name', - // 'value' => $this->paystack_api->plugin_name - // ]; - - $custom_data[] = [ - 'display_name' => 'Plugin Name', - 'variable_name' => 'plugin_name', - 'value' => 'pstk-gravityforms' - ]; - - // Generate transaction reference - $reference = uniqid("gf-{$entry['id']}-"); - - // Updating lead's payment_status to Processing - GFAPI::update_entry_property($entry['id'], 'payment_status', 'Processing'); - - // Prepare transaction data - $args = array( - 'email' => $customer_info['email'], - 'currency' => $currency, - 'amount' => $this->get_amount_export($payment_amount, $currency), - 'reference' => $reference, - 'callback_url' => $return_url, - 'description' => sprintf(__('%s (transaction: %s)', 'paystack'), $feed['meta']['feedName'], $reference), - 'metadata' => array( - 'entry_id' => $entry['id'], - 'site_url' => esc_url(get_site_url()), - 'ip_address' => $_SERVER['REMOTE_ADDR'], - 'custom_fields' => $custom_data - ) - ); - - if ($is_product) { - $line_items = rgar($submission_data, 'line_items'); - $discounts = rgar($submission_data, 'discounts'); - - // Create a Payment Request & Send Invoice - } - - if ($is_subscription) { - $plan = $this->create_plan($feed, $payment_amount, $currency); - - if (is_wp_error($plan)) { - return false; - } - - $args['plan'] = $plan['plan_code']; - - $submitted_recurring_times = rgar($submission_data, 'recurring_times'); - $invoice_limit = !empty($submitted_recurring_times) && $submitted_recurring_times !== '1' ? $submitted_recurring_times : null; - - if (!empty($invoice_limit) || $invoice_limit !== 'Infinite') { - $args['invoice_limit'] = (int) $invoice_limit; - } - - $args['channels'] = ['card']; - - gform_update_meta($entry['id'], 'paystack_plan_code', $plan['plan_code']); - } - - // Initialize the charge on Paystack's servers - this will be used to charge the user's card - $response = (object) $this->paystack_api->send_request("transaction/initialize/", $args); - - $this->log_debug(__METHOD__ . "(): Initialize Paystack Transaction:" . print_r($response, true)); - - if (!$response->status) { - return false; - } - - gform_update_meta($entry['id'], 'paystack_tx_reference', $response->data['reference']); - gform_update_meta($entry['id'], 'paystack_tx_access_code', $response->data['access_code']); - gform_update_meta($entry['id'], 'paystack_tx_auth_url', $response->data['authorization_url']); - - $this->log_debug(__METHOD__ . "(): Sending to Paystack: {$response->data['authorization_url']}"); - - return $response->data['authorization_url']; - } - - public function get_fields_meta_data($feed, $entry, $fields) - { - $data = []; - - foreach ($fields as $field) { - $field_id = $feed['meta'][$field['meta_name']]; - $value = rgar($entry, $field_id); - - if ($field['name'] == 'country') { - $value = class_exists('GF_Field_Address') ? GF_Fields::get('address')->get_country_code($value) : GFCommon::get_country_code($value); - } elseif ($field['name'] == 'state') { - $value = class_exists('GF_Field_Address') ? GF_Fields::get('address')->get_us_state_code($value) : GFCommon::get_us_state_code($value); - } - - if (!empty($value)) { - $data["{$field['name']}"] = $value; - } - } - - return $data; - } - - public function get_customer_fields() - { - return array( - array('name' => 'email', 'label' => 'Email', 'meta_name' => 'customerInformation_email'), - array('name' => 'first_name', 'label' => 'First Name', 'meta_name' => 'customerInformation_firstName'), - array('name' => 'last_name', 'label' => 'Last Name', 'meta_name' => 'customerInformation_lastName'), - ); - } - - public function get_billing_fields() - { - return array( - array('name' => 'address1', 'label' => 'Address', 'meta_name' => 'billingInformation_address'), - array('name' => 'address2', 'label' => 'Address 2', 'meta_name' => 'billingInformation_address2'), - array('name' => 'city', 'label' => 'City', 'meta_name' => 'billingInformation_city'), - array('name' => 'state', 'label' => 'State', 'meta_name' => 'billingInformation_state'), - array('name' => 'zip', 'label' => 'Zip', 'meta_name' => 'billingInformation_zip'), - array('name' => 'country', 'label' => 'Country', 'meta_name' => 'billingInformation_country'), - ); - } - - /** - * Handle cancelling the subscription from the entry detail page. - * - * @param array $entry The entry object currently being processed. - * @param array $feed The feed object currently being processed. - * - * @return bool True if successful. False if failed. - */ - public function cancel($entry, $feed) - { - // Getting mode (Live (Production) or Test (Sandbox)) - $mode = $feed['meta']['mode']; - - // Setup the Paystack API - $this->paystack_api($mode); - - if (empty($entry['transaction_id'])) { - return false; - } - - // Get Paystack subscription object. - $subscription = $this->get_subscription($entry['transaction_id']); + // Echo setting markup, if enabled. + if ($echo) { + echo $html; + } + + return $html; + } + + /** + * Prepare fields for field mapping in feed settings. + * + * @return array $fields + */ + public function billing_info_fields() + { + $fields = array( + array( + 'name' => 'address_line1', + 'label' => __('Address', 'gravityformsconstantcontact'), + 'required' => false, + 'field_type' => array('address'), + ), + array( + 'name' => 'address_line2', + 'label' => __('Address 2', 'gravityformsconstantcontact'), + 'required' => false, + 'field_type' => array('address'), + ), + array( + 'name' => 'address_city', + 'label' => __('City', 'gravityformsconstantcontact'), + 'required' => false, + 'field_type' => array('address'), + ), + array( + 'name' => 'address_state', + 'label' => __('State', 'gravityformsconstantcontact'), + 'required' => false, + 'field_type' => array('address'), + ), + array( + 'name' => 'address_zip', + 'label' => __('Zip', 'gravityformsconstantcontact'), + 'required' => false, + 'field_type' => array('address'), + ), + array( + 'name' => 'address_country', + 'label' => __('Country', 'gravityformsconstantcontact'), + 'required' => false, + 'field_type' => array('address'), + ), + ); + + return $fields; + } + + /** + * Validate the custom_meta type field. + * + * @access public + * + * @uses GFAddOn::get_posted_settings() + * @uses GFAddOn::set_field_error() + * + * @param array $field The field properties. Not used. + * + * @return void + */ + public function validate_custom_meta($field) + { + /* + * Number of keys is limited to 20. + * Interface should control this, validating just in case. + * Key names have maximum length of 40 characters. + */ + + // Get metadata from posted settings. + $settings = $this->get_posted_settings(); + $meta_data = $settings['metaData']; + + // If metadata is not defined, return. + if (empty($meta_data)) { + return; + } + + // Get number of metadata items. + $meta_count = count($meta_data); + + // If there are more than 20 metadata keys, set field error. + if ($meta_count > 20) { + $this->set_field_error(array(esc_html__('You may only have 20 custom keys.'), 'gravityformspaystack')); + return; + } + + // Loop through metadata and check the key name length (custom_key). + foreach ($meta_data as $meta) { + if (empty($meta['custom_key']) && !empty($meta['value'])) { + $this->set_field_error(array('name' => 'metaData'), esc_html__("A field has been mapped to a custom key without a name. Please enter a name for the custom key, remove the metadata item, or return the corresponding drop down to 'Select a Field'.", 'gravityformspaystack')); + break; + } else if (strlen($meta['custom_key']) > 40) { + $this->set_field_error(array('name' => 'metaData'), sprintf(esc_html__('The name of custom key %s is too long. Please shorten this to 40 characters or less.', 'gravityformspaystack'), $meta['custom_key'])); + break; + } + } + } + + /** + * Prevent the 'options' checkboxes setting being included on the feed. + * + * @access public + * + * @used-by GFPaymentAddOn::other_settings_fields() + * + * @return false + */ + public function option_choices() + { + return false; + } + + /** + * Add supported notification events. + * + * @access public + * + * @used-by GFFeedAddOn::notification_events() + * @uses GFFeedAddOn::has_feed() + * + * @param array $form The form currently being processed. + * + * @return array|false The supported notification events. False if feed cannot be found within $form. + */ + public function supported_notification_events($form) + { + // If this form does not have a Paystack feed, return false. + if (!$this->has_feed($form['id'])) { + return false; + } + + // Return Paystack notification events. + return array( + 'complete_payment' => esc_html__('Payment Completed', 'gravityformspaystack'), + 'refund_payment' => esc_html__('Payment Refunded', 'gravityformspaystack'), + 'fail_payment' => esc_html__('Payment Failed', 'gravityformspaystack'), + 'create_subscription' => esc_html__('Subscription Created', 'gravityformspaystack'), + 'cancel_subscription' => esc_html__('Subscription Canceled', 'gravityformspaystack'), + 'add_subscription_payment' => esc_html__('Subscription Payment Added', 'gravityformspaystack'), + 'fail_subscription_payment' => esc_html__('Subscription Payment Failed', 'gravityformspaystack'), + ); + } + + // # PAYSTACK TRANSACTIONS ------------------------------------------------------------------------------------------- + + /** + * Useful when developing a payment gateway that processes the payment outside of the website (i.e. Paystack Redirect). + * + * @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) + { + // Don't process redirect url if request is a Paystack return + if (!rgempty('gf_paystack_return', $_GET)) { + return false; + } + + // Getting mode (Live (Production) or Test (Sandbox)) + $mode = $feed['meta']['mode']; + + // Setup the Paystack API + $this->paystack_api($mode); + + // Get the transaction type + $transaction_type = $feed['meta']['transactionType']; + + // Getting the product status + $is_product = $feed['meta']['transactionType'] === 'product'; + + // Getting the subscription status + $is_subscription = $feed['meta']['transactionType'] === 'subscription'; + + // URL that will listen to callback from Paystack + $page_url = get_bloginfo('url'); + $ids_query = "ids={$entry['id']}|{$feed['id']}|{$form['id']}"; + $ids_query .= '&hash=' . wp_hash($ids_query); + + $return_url = add_query_arg('gf_paystack_return', base64_encode($ids_query), $page_url); + + // $setup_fee = rgar($submission_data, 'setup_fee'); + // $trial_amount = rgar($submission_data, 'trial'); + $payment_amount = rgar($submission_data, 'payment_amount'); + + // Currency + $currency = rgar($entry, 'currency'); + + // Customer Info + $customer_info = $this->get_fields_meta_data($feed, $entry, $this->get_customer_fields()); + + // Get feed custom metadata + $custom_data = $this->get_paystack_meta_data($feed, $entry, $form); + + // $custom_data[] = [ + // 'display_name' => 'Plugin Name', + // 'variable_name' => 'plugin_name', + // 'value' => $this->paystack_api->plugin_name + // ]; + + $custom_data[] = [ + 'display_name' => 'Plugin Name', + 'variable_name' => 'plugin_name', + 'value' => 'pstk-gravityforms' + ]; + + // Generate transaction reference + $reference = uniqid("gf-{$entry['id']}-"); + + // Updating lead's payment_status to Processing + GFAPI::update_entry_property($entry['id'], 'payment_status', 'Processing'); + + // Prepare transaction data + $args = array( + 'email' => $customer_info['email'], + 'currency' => $currency, + 'amount' => $this->get_amount_export($payment_amount, $currency), + 'reference' => $reference, + 'callback_url' => $return_url, + 'description' => sprintf(__('%s (transaction: %s)', 'paystack'), $feed['meta']['feedName'], $reference), + 'metadata' => array( + 'entry_id' => $entry['id'], + 'site_url' => esc_url(get_site_url()), + 'ip_address' => $_SERVER['REMOTE_ADDR'], + 'custom_fields' => $custom_data + ) + ); + + if ($is_product) { + $line_items = rgar($submission_data, 'line_items'); + $discounts = rgar($submission_data, 'discounts'); + + // Create a Payment Request & Send Invoice + } + + if ($is_subscription) { + $plan = $this->create_plan($feed, $payment_amount, $currency); + + if (is_wp_error($plan)) { + return false; + } + + $args['plan'] = $plan['plan_code']; + + $submitted_recurring_times = rgar($submission_data, 'recurring_times'); + $invoice_limit = !empty($submitted_recurring_times) && $submitted_recurring_times !== '1' ? $submitted_recurring_times : null; + + if (!empty($invoice_limit) || $invoice_limit !== 'Infinite') { + $args['invoice_limit'] = (int) $invoice_limit; + } + + $args['channels'] = ['card']; + + gform_update_meta($entry['id'], 'paystack_plan_code', $plan['plan_code']); + } + + // Initialize the charge on Paystack's servers - this will be used to charge the user's card + $response = (object) $this->paystack_api->send_request("transaction/initialize/", $args); + + $this->log_debug(__METHOD__ . "(): Initialize Paystack Transaction:" . print_r($response, true)); + + if (!$response->status) { + return false; + } + + gform_update_meta($entry['id'], 'paystack_tx_reference', $response->data['reference']); + gform_update_meta($entry['id'], 'paystack_tx_access_code', $response->data['access_code']); + gform_update_meta($entry['id'], 'paystack_tx_auth_url', $response->data['authorization_url']); + + $this->log_debug(__METHOD__ . "(): Sending to Paystack: {$response->data['authorization_url']}"); + + return $response->data['authorization_url']; + } + + public function get_fields_meta_data($feed, $entry, $fields) + { + $data = []; + + foreach ($fields as $field) { + $field_id = $feed['meta'][$field['meta_name']]; + $value = rgar($entry, $field_id); + + if ($field['name'] == 'country') { + $value = class_exists('GF_Field_Address') ? GF_Fields::get('address')->get_country_code($value) : GFCommon::get_country_code($value); + } elseif ($field['name'] == 'state') { + $value = class_exists('GF_Field_Address') ? GF_Fields::get('address')->get_us_state_code($value) : GFCommon::get_us_state_code($value); + } + + if (!empty($value)) { + $data["{$field['name']}"] = $value; + } + } + + return $data; + } + + public function get_customer_fields() + { + return array( + array('name' => 'email', 'label' => 'Email', 'meta_name' => 'customerInformation_email'), + array('name' => 'first_name', 'label' => 'First Name', 'meta_name' => 'customerInformation_firstName'), + array('name' => 'last_name', 'label' => 'Last Name', 'meta_name' => 'customerInformation_lastName'), + ); + } + + public function get_billing_fields() + { + return array( + array('name' => 'address1', 'label' => 'Address', 'meta_name' => 'billingInformation_address'), + array('name' => 'address2', 'label' => 'Address 2', 'meta_name' => 'billingInformation_address2'), + array('name' => 'city', 'label' => 'City', 'meta_name' => 'billingInformation_city'), + array('name' => 'state', 'label' => 'State', 'meta_name' => 'billingInformation_state'), + array('name' => 'zip', 'label' => 'Zip', 'meta_name' => 'billingInformation_zip'), + array('name' => 'country', 'label' => 'Country', 'meta_name' => 'billingInformation_country'), + ); + } + + /** + * Handle cancelling the subscription from the entry detail page. + * + * @param array $entry The entry object currently being processed. + * @param array $feed The feed object currently being processed. + * + * @return bool True if successful. False if failed. + */ + public function cancel($entry, $feed) + { + // Getting mode (Live (Production) or Test (Sandbox)) + $mode = $feed['meta']['mode']; - if (!$subscription['status']) { - return false; - } - - if ($subscription['data']['status'] !== 'active') { - $this->log_debug(__METHOD__ . '(): Subscription already cancelled.'); - - return true; - } - - try { - $this->paystack_api->send_request('subscription/disable', [ - 'code' => $entry['transaction_id'], - 'token' => gform_get_meta($entry['id'], 'paystack_email_token') //email token, - ]); - - $this->log_debug(__METHOD__ . '(): Subscription cancelled.'); - - return true; - } catch (\Exception $e) { - // Log error. - $this->log_error(sprintf('%s(): Unable to cancel subscription; %s', __METHOD__, $e->getMessage())); - - return false; - } - } - - /** - * Display the thank you page when there's a gf_paystack_return URL param and the charge is successful. - * - * @since 1.0 - */ - public function maybe_thankyou_page() - { - if (!$this->is_gravityforms_supported()) { - return; - } - - if ($str = sanitize_text_field(rgget('gf_paystack_return'))) { - $str = base64_decode($str); - - $this->log_debug(__METHOD__ . '(): Return callback request received. Starting to process.'); - - parse_str($str, $query); - - if (wp_hash('ids=' . $query['ids']) == $query['hash']) { - list($entry_id, $feed_id, $form_id) = explode('|', $query['ids']); - - $entry = GFAPI::get_entry($entry_id); - $form = GFAPI::get_form($form_id); - - if (is_wp_error($entry) || !$entry) { - $this->log_error(__METHOD__ . '(): Entry could not be found. Aborting.'); - - return false; - } - - $this->log_debug(__METHOD__ . '(): Entry has been found => ' . print_r($entry, true)); - - if ($entry['status'] == 'spam') { - $this->log_error(__METHOD__ . '(): Entry is marked as spam. Aborting.'); - - return false; - } - - // Get feed - $feed = $this->get_feed($feed_id); - - $this->log_debug(__METHOD__ . "(): Form {$entry['form_id']} is properly configured."); - - // Let's ignore forms that are no longer configured to use the Paystack add-on - if (!$feed || !rgar($feed, 'is_active')) { - $this->log_error(__METHOD__ . "(): Form no longer uses the Paystack Addon. Form ID: {$entry['form_id']}. Aborting."); - - return false; - } - - // Getting mode Live (Production) or Test (Sandbox) - $mode = $feed['meta']['mode']; - - $this->paystack_api($mode); - - $reference = sanitize_text_field(rgget('reference')); - - try { - $response = $this->paystack_api->send_request("transaction/verify/{$reference}", [], 'get'); - - $this->log_debug(__METHOD__ . "(): Transaction verified. " . print_r($response, 1)); - } catch (\Exception $e) { - $this->log_error(__METHOD__ . "(): Transaction could not be verified. Reason: " . $e->getMessage()); - - return new WP_Error('transaction_verification', $e->getMessage()); - } - - $charge = $response['data']; - - if (!$response || $charge['status'] !== 'success') { - // Charge Failed - $this->log_error(__METHOD__ . "(): Transaction verification failed Reason: " . $response->message); - - return false; - } - - // Log Payment successful payment from this addon to paystack - $this->paystack_api->log_transaction_success($reference); - - if (!class_exists('GFFormDisplay')) { - require_once(GFCommon::get_base_path() . '/form_display.php'); - } - - $confirmation = GFFormDisplay::handle_confirmation($form, $entry, false); - - if (is_array($confirmation) && isset($confirmation['redirect'])) { - header("Location: {$confirmation['redirect']}"); - exit; - } - - GFFormDisplay::$submission[$form_id] = array( - 'is_confirmation' => true, - 'confirmation_message' => $confirmation, - 'form' => $form, - 'lead' => $entry, - ); - } - } - } - - // # WEBHOOKS ------------------------------------------------------------------------------------------------------ - - /** - * If the Paystack callback or webhook belongs to a valid entry process the raw response into a standard Gravity Forms $action. - * - * @access public - * - * @uses GFAddOn::get_plugin_settings() - * @uses GFPaystack::get_api_mode() - * @uses GFAddOn::log_error() - * @uses GFAddOn::log_debug() - * @uses GFPaymentAddOn::get_entry_by_transaction_id() - * @uses GFPaymentAddOn::get_amount_import() - * @uses GFPaystack::get_subscription_line_item() - * @uses GFPaystack::get_captured_payment_note() - * @uses GFAPI::get_entry() - * @uses GFCommon::to_money() - * - * @return array|bool|WP_Error Return a valid GF $action or if the webhook can't be processed a WP_Error object or false. - */ - public function callback() - { - if (!$this->is_gravityforms_supported()) { - return; - } - - $event = $this->get_webhook_event(); - - if (!$event || is_wp_error($event)) { - return $event; - } - - $this->log_debug(__METHOD__ . '(): Webhook callback received. Starting to process => ' . print_r($_REQUEST, true)); - - // Get event type. - $type = $event['event']; - - switch ($type) { - case 'charge.success': - if (!isset($event['data']['id']) || isset($event['data']['metadata']['invoice_action'])) { - return false; - } - - $entry_id = rgars($event, 'data/metadata/entry_id'); - - if (!$entry_id && $reference = rgars($event, 'data/reference')) { - $entry_id = $this->get_entry_id_by_reference($reference); - } - - if (!$entry_id) { - return new WP_Error('entry_not_found', sprintf(__('Entry for transaction id: %s was not found. Webhook cannot be processed.', 'gravityformspaystack'), $action['transaction_id'])); - } - - $entry = GFAPI::get_entry($entry_id); - - if (is_wp_error($entry)) { - $this->log_error(__METHOD__ . '(): ' . $entry->get_error_message()); - - return false; - } - - $this->log_debug(__METHOD__ . '(): Entry has been found => ' . print_r($entry, true)); - - $feed = $this->get_payment_feed($entry); - - // Let's ignore forms that are no longer configured to use the Paystack add-on - if (!$feed || !rgar($feed, 'is_active')) { - $this->log_error(__METHOD__ . "(): Form no longer uses the Paystack Addon. Form ID: {$entry['form_id']}. Aborting."); - - return false; - } - - if ($feed['meta']['transactionType'] == 'subscription') { - gform_update_meta($entry_id, 'paystack_tx_auth_code', rgars($event, 'data/authorization/authorization_code')); - } else { - $action['id'] = rgars($event, 'data/id') . '_' . $type; - $action['entry_id'] = $entry_id; - $action['transaction_id'] = rgars($event, 'data/id'); - $action['amount'] = $this->get_amount_import(rgars($event, 'data/amount'), rgars($event, 'data/currency')); - $action['type'] = 'complete_payment'; - $action['ready_to_fulfill'] = !$entry['is_fulfilled'] ? true : false; - $action['payment_date'] = rgars($event, 'data/paidAt'); - $action['payment_method'] = $this->_slug; - } - - break; - case 'subscription.create': - $action['subscription_id'] = rgars($event, 'data/subscription_code'); - - try { - $subscription = $this->get_subscription($action['subscription_id']); - - $this->log_debug(__METHOD__ . "(): Subscription details " . print_r($subscription, 1)); - } catch (\Exception $e) { - $this->log_error(__METHOD__ . "(): Subscription details not found. Reason: " . $e->getMessage()); - - return new WP_Error('subscription_not_found', sprintf(__('Details for subscription id: %s was not found. Webhook cannot be processed.', 'gravityformspaystack'), $action['subscription_id'])); - } - - if (is_array($subscription['data']['invoices']) && count($subscription['data']['invoices']) >= 1) { - $transaction = $subscription['data']['invoices'][0]; - - $this->log_debug(__METHOD__ . "(): Initial transaction details " . print_r($transaction, 1)); - - $entry_id = $transaction['status'] == 'success' ? $this->get_entry_id_by_reference($transaction['reference']) : false; - } - - if (!$entry_id) { - return new WP_Error('entry_not_found', sprintf(__('Entry for subscription id: %s was not found. Webhook cannot be processed.', 'gravityformspaystack'), $action['subscription_id'])); - } - - $entry = GFAPI::get_entry($entry_id); - - if (is_wp_error($entry)) { - $this->log_error(__METHOD__ . '(): ' . $entry->get_error_message()); - - return false; - } - - gform_update_meta($entry_id, 'paystack_email_token', rgars($event, 'data/email_token')); + // Setup the Paystack API + $this->paystack_api($mode); - $action['id'] = $action['subscription_id'] . '_' . $type; - $action['entry_id'] = $entry_id; - $action['type'] = 'create_subscription'; - $action['amount'] = $this->get_amount_import(rgars($event, 'data/amount'), rgar($entry, 'currency')); - $action['payment_method'] = $this->_slug; - $action['ready_to_fulfill'] = !$entry['is_fulfilled'] ? true : false; + if (empty($entry['transaction_id'])) { + return false; + } - $action['add_subscription_payment'] = [ - 'entry_id' => $entry_id, - 'subscription_id' => rgars($subscription, 'data/subscription_code'), - 'transaction_id' => rgar($transaction, 'id'), - 'amount' => $this->get_amount_import(rgar($transaction, 'amount'), rgar($entry, 'currency')), - 'payment_method' => $this->_slug - ]; + // Get Paystack subscription object. + $subscription = $this->get_subscription($entry['transaction_id']); - break; - case 'subscription.disable': - $action['subscription_id'] = rgars($event, 'data/subscription_code'); + if (!$subscription['status']) { + return false; + } - $entry_id = $this->get_entry_by_transaction_id($action['subscription_id']); + if ($subscription['data']['status'] !== 'active') { + $this->log_debug(__METHOD__ . '(): Subscription already cancelled.'); - if (!$entry_id) { - return new WP_Error('entry_not_found', sprintf(__('Entry for subscription id: %s was not found. Webhook cannot be processed.', 'gravityformspaystack'), $action['subscription_id'])); - } + return true; + } - $entry = GFAPI::get_entry($entry_id); + try { + $this->paystack_api->send_request( + 'subscription/disable', [ + 'code' => $entry['transaction_id'], + 'token' => gform_get_meta($entry['id'], 'paystack_email_token') //email token, + ] + ); - if (is_wp_error($entry)) { - $this->log_error(__METHOD__ . '(): ' . $entry->get_error_message()); + $this->log_debug(__METHOD__ . '(): Subscription cancelled.'); - return false; - } + return true; + } catch (\Exception $e) { + // Log error. + $this->log_error(sprintf('%s(): Unable to cancel subscription; %s', __METHOD__, $e->getMessage())); - if ($entry['payment_status'] == 'Cancelled' || $entry['payment_status'] == 'Expired') { - $this->log_error(__METHOD__ . '(): Subscription for entry ' . $entry['id'] . ' is no longer Active'); + return false; + } + } - return false; - } + /** + * Display the thank you page when there's a gf_paystack_return URL param and the charge is successful. + * + * @since 1.0 + */ + public function maybe_thankyou_page() + { + if (!$this->is_gravityforms_supported()) { + return; + } + + if ($str = sanitize_text_field(rgget('gf_paystack_return'))) { + $str = base64_decode($str); + + $this->log_debug(__METHOD__ . '(): Return callback request received. Starting to process.'); + + parse_str($str, $query); + + if (wp_hash('ids=' . $query['ids']) == $query['hash']) { + list($entry_id, $feed_id, $form_id) = explode('|', $query['ids']); + + $entry = GFAPI::get_entry($entry_id); + $form = GFAPI::get_form($form_id); + + if (is_wp_error($entry) || !$entry) { + $this->log_error(__METHOD__ . '(): Entry could not be found. Aborting.'); + + return false; + } + + $this->log_debug(__METHOD__ . '(): Entry has been found => ' . print_r($entry, true)); + + if ($entry['status'] == 'spam') { + $this->log_error(__METHOD__ . '(): Entry is marked as spam. Aborting.'); + + return false; + } + + // Get feed + $feed = $this->get_feed($feed_id); + + $this->log_debug(__METHOD__ . "(): Form {$entry['form_id']} is properly configured."); + + // Let's ignore forms that are no longer configured to use the Paystack add-on + if (!$feed || !rgar($feed, 'is_active')) { + $this->log_error(__METHOD__ . "(): Form no longer uses the Paystack Addon. Form ID: {$entry['form_id']}. Aborting."); + + return false; + } + + // Getting mode Live (Production) or Test (Sandbox) + $mode = $feed['meta']['mode']; - $action['entry_id'] = $entry_id; + $this->paystack_api($mode); - try { - $subscription = $this->get_subscription($action['subscription_id']); + $reference = sanitize_text_field(rgget('reference')); - $paid_invoices = 0; + try { + $response = $this->paystack_api->send_request("transaction/verify/{$reference}", [], 'get'); - if (is_array($subscription['data']['invoices']) && $invoices = $subscription['data']['invoices']) { - $this->log_debug(__METHOD__ . '(): Subscription Invoices: ' . print_r($invoices, 1)); + $this->log_debug(__METHOD__ . "(): Transaction verified. " . print_r($response, 1)); + } catch (\Exception $e) { + $this->log_error(__METHOD__ . "(): Transaction could not be verified. Reason: " . $e->getMessage()); - foreach ($invoices as $invoice) { - if ($invoice['status'] == 'success') { - $paid_invoices++; - } - } - } + return new WP_Error('transaction_verification', $e->getMessage()); + } - $invoice_limit = (int) rgars($subscription, 'data/invoice_limit'); + $charge = $response['data']; - if ($invoice_limit > 0 && $paid_invoices >= $invoice_limit) { - $action['type'] = 'expire_subscription'; - } else { - $action['type'] = 'cancel_subscription'; - } - } catch (\Exception $e) { - $this->log_error(__METHOD__ . "(): Paystack Subscription details not found. Reason: " . $e->getMessage() . 'So we cancel the subscrption.'); + if (!$response || $charge['status'] !== 'success') { + // Charge Failed + $this->log_error(__METHOD__ . "(): Transaction verification failed Reason: " . $response->message); - $action['type'] = 'cancel_subscription'; + return false; + } - // return new WP_Error('subscription_not_found', sprintf(__('Details for subscription id: %s was not found. Webhook cannot be processed.', 'gravityformspaystack'), $action['subscription_id'])); - } + // Log Payment successful payment from this addon to paystack + $this->paystack_api->log_transaction_success($reference); - break; + if (!class_exists('GFFormDisplay')) { + include_once GFCommon::get_base_path() . '/form_display.php'; + } - case 'invoice.create': - case 'invoice.update': - $action['subscription_id'] = rgars($event, 'data/subscription/subscription_code'); + $confirmation = GFFormDisplay::handle_confirmation($form, $entry, false); - $entry_id = $this->get_entry_by_transaction_id($action['subscription_id']); + if (is_array($confirmation) && isset($confirmation['redirect'])) { + header("Location: {$confirmation['redirect']}"); + exit; + } - if (!$entry_id) { - return new WP_Error('entry_not_found', sprintf(__('Entry for transaction id: %s was not found. Webhook cannot be processed.', 'gravityformspaystack'), $action['transaction_id'])); - } + GFFormDisplay::$submission[$form_id] = array( + 'is_confirmation' => true, + 'confirmation_message' => $confirmation, + 'form' => $form, + 'lead' => $entry, + ); + } + } + } - $entry = GFAPI::get_entry($entry_id); + // # WEBHOOKS ------------------------------------------------------------------------------------------------------ - if (is_wp_error($entry)) { - $this->log_error(__METHOD__ . '(): ' . $entry->get_error_message()); + /** + * If the Paystack callback or webhook belongs to a valid entry process the raw response into a standard Gravity Forms $action. + * + * @access public + * + * @uses GFAddOn::get_plugin_settings() + * @uses GFPaystack::get_api_mode() + * @uses GFAddOn::log_error() + * @uses GFAddOn::log_debug() + * @uses GFPaymentAddOn::get_entry_by_transaction_id() + * @uses GFPaymentAddOn::get_amount_import() + * @uses GFPaystack::get_subscription_line_item() + * @uses GFPaystack::get_captured_payment_note() + * @uses GFAPI::get_entry() + * @uses GFCommon::to_money() + * + * @return array|bool|WP_Error Return a valid GF $action or if the webhook can't be processed a WP_Error object or false. + */ + public function callback() + { + if (!$this->is_gravityforms_supported()) { + return; + } - return false; - } + $event = $this->get_webhook_event(); - try { - $subscription = $this->get_subscription($action['subscription_id']); - } catch (\Exception $e) { - $this->log_error(__METHOD__ . "(): Subscription details not found. Reason: " . $e->getMessage()); + if (!$event || is_wp_error($event)) { + return $event; + } - return new WP_Error('subscription_not_found', sprintf(__('Details for subscription id: %s was not found. Webhook cannot be processed.', 'gravityformspaystack'), $action['subscription_id'])); - } + $this->log_debug(__METHOD__ . '(): Webhook callback received. Starting to process => ' . print_r($_REQUEST, true)); - $reference = rgars($event, 'data/transaction/reference'); + // Get event type. + $type = $event['event']; - if (is_array($subscription['data']['invoices'])) { - $key = array_search($reference, array_column($subscription['data']['invoices'], 'reference')); + switch ($type) { + case 'charge.success': + if (!isset($event['data']['id']) || isset($event['data']['metadata']['invoice_action'])) { + return false; + } - $transaction = $subscription['data']['invoices'][$key]; - } else { - $transaction = $this->paystack_api->send_request("transaction/verify/{$reference}", [], 'get'); - } + $entry_id = rgars($event, 'data/metadata/entry_id'); - $action['type'] = 'add_subscription_payment'; - $action['subscription_id'] = rgar($entry, 'transaction_id'); - $action['transaction_id'] = rgars($transaction, 'id'); - $action['amount'] = $this->get_amount_import(rgar($transaction, 'amount'), rgar($entry, 'currency')); - $action['entry_id'] = $entry_id; - $action['payment_method'] = $this->_slug; + if (!$entry_id && $reference = rgars($event, 'data/reference')) { + $entry_id = $this->get_entry_id_by_reference($reference); + } - break; + if (!$entry_id) { + return new WP_Error('entry_not_found', sprintf(__('Entry for transaction id: %s was not found. Webhook cannot be processed.', 'gravityformspaystack'), $action['transaction_id'])); + } - case 'invoice.payment_failed': - $action['subscription_id'] = rgars($event, 'data/subscription/subscription_code'); + $entry = GFAPI::get_entry($entry_id); - //no transaction created - $action['id'] = $action['subscription_id'] . '_' . rgars($event, 'data/invoice_code') . '_' . $type; + if (is_wp_error($entry)) { + $this->log_error(__METHOD__ . '(): ' . $entry->get_error_message()); - $entry_id = $this->get_entry_by_transaction_id($action['subscription_id']); + return false; + } - if (!$entry_id) { - return new WP_Error('entry_not_found', sprintf(__('Entry for transaction id: %s was not found. Webhook cannot be processed.', 'gravityformspaystack'), $action['transaction_id'])); - } + $this->log_debug(__METHOD__ . '(): Entry has been found => ' . print_r($entry, true)); - $entry = GFAPI::get_entry($entry_id); + $feed = $this->get_payment_feed($entry); - if (is_wp_error($entry)) { - $this->log_error(__METHOD__ . '(): ' . $entry->get_error_message()); + // Let's ignore forms that are no longer configured to use the Paystack add-on + if (!$feed || !rgar($feed, 'is_active')) { + $this->log_error(__METHOD__ . "(): Form no longer uses the Paystack Addon. Form ID: {$entry['form_id']}. Aborting."); - return false; - } + return false; + } - $action['type'] = 'fail_subscription_payment'; - $action['entry_id'] = $entry['id']; - $action['amount'] = $this->get_amount_import(rgars($event, 'data/amount'), rgar($entry, 'currency')); - $action['payment_method'] = $this->_slug; + if ($feed['meta']['transactionType'] == 'subscription') { + gform_update_meta($entry_id, 'paystack_tx_auth_code', rgars($event, 'data/authorization/authorization_code')); + } else { + $action['id'] = rgars($event, 'data/id') . '_' . $type; + $action['entry_id'] = $entry_id; + $action['transaction_id'] = rgars($event, 'data/id'); + $action['amount'] = $this->get_amount_import(rgars($event, 'data/amount'), rgars($event, 'data/currency')); + $action['type'] = 'complete_payment'; + $action['ready_to_fulfill'] = !$entry['is_fulfilled'] ? true : false; + $action['payment_date'] = rgars($event, 'data/paidAt'); + $action['payment_method'] = $this->_slug; + } + + break; + case 'subscription.create': + $action['subscription_id'] = rgars($event, 'data/subscription_code'); - break; - } + try { + $subscription = $this->get_subscription($action['subscription_id']); - if (rgempty('entry_id', $action)) { - $this->log_debug(__METHOD__ . '() entry_id not set for callback action; no further processing required.'); + $this->log_debug(__METHOD__ . "(): Subscription details " . print_r($subscription, 1)); + } catch (\Exception $e) { + $this->log_error(__METHOD__ . "(): Subscription details not found. Reason: " . $e->getMessage()); - return false; - } + return new WP_Error('subscription_not_found', sprintf(__('Details for subscription id: %s was not found. Webhook cannot be processed.', 'gravityformspaystack'), $action['subscription_id'])); + } - return $action; - } + if (is_array($subscription['data']['invoices']) && count($subscription['data']['invoices']) >= 1) { + $transaction = $subscription['data']['invoices'][0]; - /** - * Helper for making the gform_post_payment_action hook available to the various payment interaction methods. Also handles sending notifications for payment events. - * - * @param array $entry - * @param array $action - */ - public function post_payment_action($entry, $action) - { - parent::post_payment_action($entry, $action); + $this->log_debug(__METHOD__ . "(): Initial transaction details " . print_r($transaction, 1)); - if (isset($action['add_subscription_payment']) && is_array($action['add_subscription_payment'])) { - $this->add_subscription_payment($entry, $action['add_subscription_payment']); - } - } + $entry_id = $transaction['status'] == 'success' ? $this->get_entry_id_by_reference($transaction['reference']) : false; + } - /** - * Retrieve the Paystack Event for the received webhook. - * - * - * @return false|WP_Error - */ - public function get_webhook_event() - { - // Retrieve the request's payload - $body = @file_get_contents('php://input'); + if (!$entry_id) { + return new WP_Error('entry_not_found', sprintf(__('Entry for subscription id: %s was not found. Webhook cannot be processed.', 'gravityformspaystack'), $action['subscription_id'])); + } - $response = json_decode($body, true); + $entry = GFAPI::get_entry($entry_id); - // Get api mode from data - $mode = rgars($response, 'data/domain'); + if (is_wp_error($entry)) { + $this->log_error(__METHOD__ . '(): ' . $entry->get_error_message()); - $this->paystack_api($mode); + return false; + } - // Validate Webhook Request Payload - try { - $is_verified = $this->paystack_api->validate_webhook($body); - } catch (\Exception $e) { - $this->log_error(__METHOD__ . "(): Error: " . $e->getMessage()); + gform_update_meta($entry_id, 'paystack_email_token', rgars($event, 'data/email_token')); - return new WP_Error('Webhook Validation Failed', $e->getMessage()); - } + $action['id'] = $action['subscription_id'] . '_' . $type; + $action['entry_id'] = $entry_id; + $action['type'] = 'create_subscription'; + $action['amount'] = $this->get_amount_import(rgars($event, 'data/amount'), rgar($entry, 'currency')); + $action['payment_method'] = $this->_slug; + $action['ready_to_fulfill'] = !$entry['is_fulfilled'] ? true : false; - if (!$is_verified) { - $this->log_error(__METHOD__ . '(): Wehhook request is invalid. Aborting.'); + $action['add_subscription_payment'] = [ + 'entry_id' => $entry_id, + 'subscription_id' => rgars($subscription, 'data/subscription_code'), + 'transaction_id' => rgar($transaction, 'id'), + 'amount' => $this->get_amount_import(rgar($transaction, 'amount'), rgar($entry, 'currency')), + 'payment_method' => $this->_slug + ]; - return false; - } + break; + case 'subscription.disable': + $action['subscription_id'] = rgars($event, 'data/subscription_code'); - $this->log_debug(__METHOD__ . "(): Processing $mode mode event."); + $entry_id = $this->get_entry_by_transaction_id($action['subscription_id']); - return $response; - } + if (!$entry_id) { + return new WP_Error('entry_not_found', sprintf(__('Entry for subscription id: %s was not found. Webhook cannot be processed.', 'gravityformspaystack'), $action['subscription_id'])); + } - /** - * Generate the url Paystack webhooks should be sent to. - * - * @access public - * - * @used-by GFPaystack::get_webhooks_section_description() - * - * @param int $feed_id The feed id. - * - * @return string The webhook URL. - */ - public function get_webhook_url($feed_id = null) - { - $url = home_url('/', 'https') . '?callback=' . $this->_slug; + $entry = GFAPI::get_entry($entry_id); - if (!rgblank($feed_id)) { - $url .= '&fid=' . $feed_id; - } - - return $url; - } + if (is_wp_error($entry)) { + $this->log_error(__METHOD__ . '(): ' . $entry->get_error_message()); - /** - * Helper to check that webhooks are enabled. - * - * @access public - * - * @used-by GFPaystack::can_create_feed() - * @uses GFAddOn::get_plugin_setting() - * - * @return bool True if webhook is enabled. False otherwise. - */ - public function is_webhook_enabled() - { - return $this->get_plugin_setting('webhooks_enabled') == true; - } - - // # PAYSTACK HELPER FUNCTIONS --------------------------------------------------------------------------------------- - - /** - * Try and retrieve the plan if a plan with the matching id has previously been created. - * - * @access public - * - * @param string $plan_id The subscription plan id or code. - * - * @return array|bool|string $plan The plan details. False if not found. If invalid request, the error message. - */ - public function get_plan($plan_id_or_code) - { - // Get Paystack plan. - $response = (object) $this->paystack_api->send_request("plan/{$plan_id_or_code}", [], 'get'); + return false; + } - $plan = $response->data; + if ($entry['payment_status'] == 'Cancelled' || $entry['payment_status'] == 'Expired') { + $this->log_error(__METHOD__ . '(): Subscription for entry ' . $entry['id'] . ' is no longer Active'); - return $plan; - } + return false; + } - /** - * Create and return a Paystack plan with the specified properties. - * - * @access public - * - * @uses GFPaymentAddOn::get_amount_export() - * @uses GFAddOn::log_debug() - * - * @param array $feed The feed currently being processed. - * @param float|int $payment_amount The recurring amount. - * @param int $trial_period_days The number of days the trial should last. - * @param string $currency The currency code for the entry being processed. - * - * @return array The plan object. - */ - public function create_plan($feed, $payment_amount, $currency) - { - // Prepare plan data. - $billing_cycle = rgar($feed['meta'], 'billingCycle_unit'); - - $name = $this->get_plan_name($feed, $billing_cycle, $payment_amount, $currency); - $amount = $this->get_amount_export($payment_amount, $currency); - - $recurring_times = (int) rgar($feed['meta'], 'recurringTimes'); - $send_invoices = (int) rgar($feed['meta'], 'sendInvoices') == 1 ? 'true' : 'false'; - - $args = array( - 'name' => $name, - 'currency' => $currency, - 'amount' => $amount, - 'interval' => $billing_cycle, - 'invoice_limit' => $recurring_times, - 'send_invoices' => $send_invoices - ); - - // Log the plan to be created. - $this->log_debug(__METHOD__ . '(): Plan to be created => ' . print_r($args, true)); - - // Create Paystack plan. - try { - $response = (object) $this->paystack_api->send_request('plan', $args); - } catch (\Exception $e) { - // Get error response. - $this->log_debug(__METHOD__ . '(): Unable to create Plan. Reason: ' . $e->getMessage()); - - return new WP_Error('plan_not_created', $e->getMessage()); - } + $action['entry_id'] = $entry_id; - $this->log_debug(__METHOD__ . "(): Plan Created. " . print_r($response, true)); + try { + $subscription = $this->get_subscription($action['subscription_id']); - return $response->data; - } + $paid_invoices = 0; - public function get_plan_name($feed, $billing_cycle, $payment_amount, $currency = '') - { - $get_name = rgars($feed, 'meta/planName') ?? $feed['meta']['feedName']; + if (is_array($subscription['data']['invoices']) && $invoices = $subscription['data']['invoices']) { + $this->log_debug(__METHOD__ . '(): Subscription Invoices: ' . print_r($invoices, 1)); - $plan_name = implode('_', array_filter(array($get_name, $feed['id'], $billing_cycle, $payment_amount, $currency))); + foreach ($invoices as $invoice) { + if ($invoice['status'] == 'success') { + $paid_invoices++; + } + } + } - return $plan_name; - } + $invoice_limit = (int) rgars($subscription, 'data/invoice_limit'); - /** - * Gets the Paystack subscription object for the given ID. - * - * @param string $subscription_id The subscription ID. - * - * @return bool|array - */ - public function get_subscription($subscription_id_or_code) - { - $this->log_debug(__METHOD__ . '(): Getting subscription ' . $subscription_id_or_code); + if ($invoice_limit > 0 && $paid_invoices >= $invoice_limit) { + $action['type'] = 'expire_subscription'; + } else { + $action['type'] = 'cancel_subscription'; + } + } catch (\Exception $e) { + $this->log_error(__METHOD__ . "(): Paystack Subscription details not found. Reason: " . $e->getMessage() . 'So we cancel the subscrption.'); - try { - $subscription = $this->paystack_api->send_request("subscription/{$subscription_id_or_code}", [], 'get'); - } catch (\Exception $e) { - $this->log_error(__METHOD__ . '(): Unable to get subscription. Reason: ' . $e->getMessage()); + $action['type'] = 'cancel_subscription'; - $subscription = false; - } + // return new WP_Error('subscription_not_found', sprintf(__('Details for subscription id: %s was not found. Webhook cannot be processed.', 'gravityformspaystack'), $action['subscription_id'])); + } - return $subscription; - } + break; - /** - * If custom meta data has been configured on the feed retrieve the mapped field values. - * - * @access public - * - * @uses GFAddOn::get_field_value() - * - * @param array $feed The feed object currently being processed. - * @param array $entry The entry object currently being processed. - * @param array $form The form object currently being processed. - * - * @return array The Paystack meta data. - */ - public function get_paystack_meta_data($feed, $entry, $form) - { - // Initialize metadata array. - $metadata = array(); - - // Find feed metadata. - $custom_meta = rgars($feed, 'meta/metaData'); - - if (is_array($custom_meta)) { - // Loop through custom meta and add to metadata for paystack. - foreach ($custom_meta as $meta) { - - // If custom key or value are empty, skip meta. - if (empty($meta['custom_key']) || empty($meta['value'])) { - continue; - } - - // Make the key available to the gform_paystack_field_value filter. - $this->_current_meta_key = $meta['custom_key']; - - // Get field value for meta key. - $field_value = $this->get_field_value($form, $entry, $meta['value']); - - if (!empty($field_value)) { - // Trim to 500 characters. - $field_value = substr($field_value, 0, 500); - - // Add to metadata array. - $metadata[] = [ - 'display_name' => $meta['custom_key'], - 'variable_name' => sanitize_title($meta['custom_key']), - 'value' => $field_value, - ]; - } - } + case 'invoice.create': + case 'invoice.update': + $action['subscription_id'] = rgars($event, 'data/subscription/subscription_code'); - // Clear the key in case get_field_value() and gform_paystack_field_value are used elsewhere. - $this->_current_meta_key = ''; - } + $entry_id = $this->get_entry_by_transaction_id($action['subscription_id']); - return $metadata; - } + if (!$entry_id) { + return new WP_Error('entry_not_found', sprintf(__('Entry for transaction id: %s was not found. Webhook cannot be processed.', 'gravityformspaystack'), $action['transaction_id'])); + } - /** - * Initialize the Paystack API and returns the GF Paysack API Object. - * - * @access public - * - * @used-by GFPaystack::cancel() - * @used-by GFPaystack::get_paystack_event() - * @used-by GFPaystack::subscribe() - * @uses GFAddOn::get_base_path() - * @uses GFPaystack::get_secret_api_key() - * @uses GFPaystack::get_public_api_key() - * - * @param null|string $mode The API mode; live or test. - * @param null|array $settings The settings. - * - * @return \GFPaystackApi - */ - public function paystack_api($mode = null, $settings = null) - { - if (empty($settings)) { - $settings = $this->get_plugin_settings(); - } + $entry = GFAPI::get_entry($entry_id); - if (empty($mode)) { - $mode = $this->get_api_mode($settings); - } + if (is_wp_error($entry)) { + $this->log_error(__METHOD__ . '(): ' . $entry->get_error_message()); - $config = (object) array( - 'secret_key' => $this->get_secret_api_key($mode, $settings), - 'public_key' => $this->get_public_api_key($mode, $settings) - ); + return false; + } - $this->log_debug(sprintf('%s(): Initializing Paystack API for %s mode.', __METHOD__, $mode)); + try { + $subscription = $this->get_subscription($action['subscription_id']); + } catch (\Exception $e) { + $this->log_error(__METHOD__ . "(): Subscription details not found. Reason: " . $e->getMessage()); - return $this->paystack_api = new GFPaystackApi($config); - } + return new WP_Error('subscription_not_found', sprintf(__('Details for subscription id: %s was not found. Webhook cannot be processed.', 'gravityformspaystack'), $action['subscription_id'])); + } - // # OTHER HELPER FUNCTIONS ---------------------------------------------------------------------------------------------- - - /** - * Retrieve the specified api key. - * - * @access public - * - * @used-by GFPaystack::get_public_api_key() - * @used-by GFPaystack::get_secret_api_key() - * @uses GFPaystack::get_query_string_api_key() - * @uses GFAddOn::get_plugin_settings() - * @uses GFPaystack::get_api_mode() - * @uses GFAddOn::get_setting() - * - * @param string $type The type of key to retrieve. - * @param null|string $mode The API mode; live or test. - * @param null|int $settings The current settings. - * - * @return string - */ - public function get_api_key($type = 'secret', $mode = null, $settings = null) - { - // Check for api key in query first; user must be an administrator to use this feature. - $api_key = $this->get_query_string_api_key($type); - if ($api_key && current_user_can('update_core')) { - return $api_key; - } + $reference = rgars($event, 'data/transaction/reference'); - if (!isset($settings)) { - $settings = $this->get_plugin_settings(); + if (is_array($subscription['data']['invoices'])) { + $key = array_search($reference, array_column($subscription['data']['invoices'], 'reference')); - if (!$mode) { - // Get API mode. - $mode = $this->get_api_mode($settings); - } - } + $transaction = $subscription['data']['invoices'][$key]; + } else { + $transaction = $this->paystack_api->send_request("transaction/verify/{$reference}", [], 'get'); + } - // Get API key based on current mode and defined type. - $setting_key = "{$mode}_{$type}_key"; - $api_key = $this->get_setting($setting_key, '', $settings); + $action['type'] = 'add_subscription_payment'; + $action['subscription_id'] = rgar($entry, 'transaction_id'); + $action['transaction_id'] = rgars($transaction, 'id'); + $action['amount'] = $this->get_amount_import(rgar($transaction, 'amount'), rgar($entry, 'currency')); + $action['entry_id'] = $entry_id; + $action['payment_method'] = $this->_slug; - return $api_key; - } + break; - /** - * Helper to implement the gform_paystack_api_mode filter so the api mode can be overridden. - * - * @access public - * - * @used-by GFPaystack::get_api_key() - * @used-by GFPaystack::callback() - * @used-by GFPaystack::can_create_feed() - * - * @param array $settings The plugin settings. - * @param int $feed_id Feed ID. - * - * @return string $api_mode Either live or test. - */ - public function get_api_mode($settings, $feed_id = null) - { - // Get API mode from settings. - $api_mode = rgar($settings, 'api_mode'); - - /** - * Filters the API mode. - * - * @param string $api_mode The API mode. - * @param int $feed_id Feed ID. - */ - return apply_filters('gform_paystack_api_mode', $api_mode, $feed_id); - } + case 'invoice.payment_failed': + $action['subscription_id'] = rgars($event, 'data/subscription/subscription_code'); - /** - * Retrieve the specified api key from the query string. - * - * @access public - * - * @used-by GFPaystack::get_api_key() - * - * @param string $type The type of key to retrieve. Defaults to 'secret'. - * - * @return string The result of the query string. - */ - public function get_query_string_api_key($type) - { - return rgget($type); - } + //no transaction created + $action['id'] = $action['subscription_id'] . '_' . rgars($event, 'data/invoice_code') . '_' . $type; - /** - * Retrieve the public api key. - * - * @access public - * - * @uses GFPaystack::get_api_key() - * - * @param null|string $mode The API mode; live or test. - * @param array|null $settings The current settings. - * - * @return string The public API key. - */ - public function get_public_api_key($mode = null, $settings = null) - { - if (empty($settings)) { - $settings = $this->get_plugin_settings(); - } + $entry_id = $this->get_entry_by_transaction_id($action['subscription_id']); - if (empty($mode)) { - $mode = $this->get_api_mode($settings); - } + if (!$entry_id) { + return new WP_Error('entry_not_found', sprintf(__('Entry for transaction id: %s was not found. Webhook cannot be processed.', 'gravityformspaystack'), $action['transaction_id'])); + } - return $this->get_api_key('public', $mode, $settings); - } + $entry = GFAPI::get_entry($entry_id); - /** - * Retrieve the secret api key. - * - * @access public - * - * @uses GFPaystack::get_api_key() - * - * @param null|string $mode The API mode; live or test. - * @param null|array $settings The current settings. - * - * @return string The secret API key. - */ - public function get_secret_api_key($mode = null, $settings = null) - { - if (empty($settings)) { - $settings = $this->get_plugin_settings(); - } + if (is_wp_error($entry)) { + $this->log_error(__METHOD__ . '(): ' . $entry->get_error_message()); - if (empty($mode)) { - $mode = $this->get_api_mode($settings); - } + return false; + } - return $this->get_api_key('secret', $mode, $settings); - } + $action['type'] = 'fail_subscription_payment'; + $action['entry_id'] = $entry['id']; + $action['amount'] = $this->get_amount_import(rgars($event, 'data/amount'), rgar($entry, 'currency')); + $action['payment_method'] = $this->_slug; + + break; + } - /** - * Adds the currency if it isn't already registered. - * - * @since 1.0 - * @access public - * @used-by gform_currencies - * - * @param array $currencies The current currencies registered in Gravity Forms. - * - * @return array List of supported currencies. - */ + if (rgempty('entry_id', $action)) { + $this->log_debug(__METHOD__ . '() entry_id not set for callback action; no further processing required.'); + + return false; + } + + return $action; + } + + /** + * Helper for making the gform_post_payment_action hook available to the various payment interaction methods. Also handles sending notifications for payment events. + * + * @param array $entry + * @param array $action + */ + public function post_payment_action($entry, $action) + { + parent::post_payment_action($entry, $action); + + if (isset($action['add_subscription_payment']) && is_array($action['add_subscription_payment'])) { + $this->add_subscription_payment($entry, $action['add_subscription_payment']); + } + } + + /** + * Retrieve the Paystack Event for the received webhook. + * + * @return false|WP_Error + */ + public function get_webhook_event() + { + // Retrieve the request's payload + $body = @file_get_contents('php://input'); + + $response = json_decode($body, true); + + // Get api mode from data + $mode = rgars($response, 'data/domain'); + + $this->paystack_api($mode); + + // Validate Webhook Request Payload + try { + $is_verified = $this->paystack_api->validate_webhook($body); + } catch (\Exception $e) { + $this->log_error(__METHOD__ . "(): Error: " . $e->getMessage()); + + return new WP_Error('Webhook Validation Failed', $e->getMessage()); + } + + if (!$is_verified) { + $this->log_error(__METHOD__ . '(): Wehhook request is invalid. Aborting.'); + + return false; + } + + $this->log_debug(__METHOD__ . "(): Processing $mode mode event."); + + return $response; + } + + /** + * Generate the url Paystack webhooks should be sent to. + * + * @access public + * + * @used-by GFPaystack::get_webhooks_section_description() + * + * @param int $feed_id The feed id. + * + * @return string The webhook URL. + */ + public function get_webhook_url($feed_id = null) + { + $url = home_url('/', 'https') . '?callback=' . $this->_slug; + + if (!rgblank($feed_id)) { + $url .= '&fid=' . $feed_id; + } + + return $url; + } + + /** + * Helper to check that webhooks are enabled. + * + * @access public + * + * @used-by GFPaystack::can_create_feed() + * @uses GFAddOn::get_plugin_setting() + * + * @return bool True if webhook is enabled. False otherwise. + */ + public function is_webhook_enabled() + { + return $this->get_plugin_setting('webhooks_enabled') == true; + } + + // # PAYSTACK HELPER FUNCTIONS --------------------------------------------------------------------------------------- + + /** + * Try and retrieve the plan if a plan with the matching id has previously been created. + * + * @access public + * + * @param string $plan_id The subscription plan id or code. + * + * @return array|bool|string $plan The plan details. False if not found. If invalid request, the error message. + */ + public function get_plan($plan_id_or_code) + { + // Get Paystack plan. + $response = (object) $this->paystack_api->send_request("plan/{$plan_id_or_code}", [], 'get'); + + $plan = $response->data; + + return $plan; + } + + /** + * Create and return a Paystack plan with the specified properties. + * + * @access public + * + * @uses GFPaymentAddOn::get_amount_export() + * @uses GFAddOn::log_debug() + * + * @param array $feed The feed currently being processed. + * @param float|int $payment_amount The recurring amount. + * @param int $trial_period_days The number of days the trial should last. + * @param string $currency The currency code for the entry being processed. + * + * @return array The plan object. + */ + public function create_plan($feed, $payment_amount, $currency) + { + // Prepare plan data. + $billing_cycle = rgar($feed['meta'], 'billingCycle_unit'); + + $name = $this->get_plan_name($feed, $billing_cycle, $payment_amount, $currency); + $amount = $this->get_amount_export($payment_amount, $currency); + + $recurring_times = (int) rgar($feed['meta'], 'recurringTimes'); + $send_invoices = (int) rgar($feed['meta'], 'sendInvoices') == 1 ? 'true' : 'false'; + + $args = array( + 'name' => $name, + 'currency' => $currency, + 'amount' => $amount, + 'interval' => $billing_cycle, + 'invoice_limit' => $recurring_times, + 'send_invoices' => $send_invoices + ); + + // Log the plan to be created. + $this->log_debug(__METHOD__ . '(): Plan to be created => ' . print_r($args, true)); + + // Create Paystack plan. + try { + $response = (object) $this->paystack_api->send_request('plan', $args); + } catch (\Exception $e) { + // Get error response. + $this->log_debug(__METHOD__ . '(): Unable to create Plan. Reason: ' . $e->getMessage()); + + return new WP_Error('plan_not_created', $e->getMessage()); + } + + $this->log_debug(__METHOD__ . "(): Plan Created. " . print_r($response, true)); + + return $response->data; + } + + public function get_plan_name($feed, $billing_cycle, $payment_amount, $currency = '') + { + $get_name = rgars($feed, 'meta/planName') ?? $feed['meta']['feedName']; + + $plan_name = implode('_', array_filter(array($get_name, $feed['id'], $billing_cycle, $payment_amount, $currency))); + + return $plan_name; + } + + /** + * Gets the Paystack subscription object for the given ID. + * + * @param string $subscription_id The subscription ID. + * + * @return bool|array + */ + public function get_subscription($subscription_id_or_code) + { + $this->log_debug(__METHOD__ . '(): Getting subscription ' . $subscription_id_or_code); + + try { + $subscription = $this->paystack_api->send_request("subscription/{$subscription_id_or_code}", [], 'get'); + } catch (\Exception $e) { + $this->log_error(__METHOD__ . '(): Unable to get subscription. Reason: ' . $e->getMessage()); + + $subscription = false; + } + + return $subscription; + } + + /** + * If custom meta data has been configured on the feed retrieve the mapped field values. + * + * @access public + * + * @uses GFAddOn::get_field_value() + * + * @param array $feed The feed object currently being processed. + * @param array $entry The entry object currently being processed. + * @param array $form The form object currently being processed. + * + * @return array The Paystack meta data. + */ + public function get_paystack_meta_data($feed, $entry, $form) + { + // Initialize metadata array. + $metadata = array(); + + // Find feed metadata. + $custom_meta = rgars($feed, 'meta/metaData'); + + if (is_array($custom_meta)) { + // Loop through custom meta and add to metadata for paystack. + foreach ($custom_meta as $meta) { + + // If custom key or value are empty, skip meta. + if (empty($meta['custom_key']) || empty($meta['value'])) { + continue; + } + + // Make the key available to the gform_paystack_field_value filter. + $this->_current_meta_key = $meta['custom_key']; + + // Get field value for meta key. + $field_value = $this->get_field_value($form, $entry, $meta['value']); + + if (!empty($field_value)) { + // Trim to 500 characters. + $field_value = substr($field_value, 0, 500); + + // Add to metadata array. + $metadata[] = [ + 'display_name' => $meta['custom_key'], + 'variable_name' => sanitize_title($meta['custom_key']), + 'value' => $field_value, + ]; + } + } + + // Clear the key in case get_field_value() and gform_paystack_field_value are used elsewhere. + $this->_current_meta_key = ''; + } + + return $metadata; + } + + /** + * Initialize the Paystack API and returns the GF Paysack API Object. + * + * @access public + * + * @used-by GFPaystack::cancel() + * @used-by GFPaystack::get_paystack_event() + * @used-by GFPaystack::subscribe() + * @uses GFAddOn::get_base_path() + * @uses GFPaystack::get_secret_api_key() + * @uses GFPaystack::get_public_api_key() + * + * @param null|string $mode The API mode; live or test. + * @param null|array $settings The settings. + * + * @return \GFPaystackApi + */ + public function paystack_api($mode = null, $settings = null) + { + if (empty($settings)) { + $settings = $this->get_plugin_settings(); + } + + if (empty($mode)) { + $mode = $this->get_api_mode($settings); + } + + $config = (object) array( + 'secret_key' => $this->get_secret_api_key($mode, $settings), + 'public_key' => $this->get_public_api_key($mode, $settings) + ); + + $this->log_debug(sprintf('%s(): Initializing Paystack API for %s mode.', __METHOD__, $mode)); + + return $this->paystack_api = new GFPaystackApi($config); + } + + // # OTHER HELPER FUNCTIONS ---------------------------------------------------------------------------------------------- + + /** + * Retrieve the specified api key. + * + * @access public + * + * @used-by GFPaystack::get_public_api_key() + * @used-by GFPaystack::get_secret_api_key() + * @uses GFPaystack::get_query_string_api_key() + * @uses GFAddOn::get_plugin_settings() + * @uses GFPaystack::get_api_mode() + * @uses GFAddOn::get_setting() + * + * @param string $type The type of key to retrieve. + * @param null|string $mode The API mode; live or test. + * @param null|int $settings The current settings. + * + * @return string + */ + public function get_api_key($type = 'secret', $mode = null, $settings = null) + { + // Check for api key in query first; user must be an administrator to use this feature. + $api_key = $this->get_query_string_api_key($type); + if ($api_key && current_user_can('update_core')) { + return $api_key; + } + + if (!isset($settings)) { + $settings = $this->get_plugin_settings(); + + if (!$mode) { + // Get API mode. + $mode = $this->get_api_mode($settings); + } + } + + // Get API key based on current mode and defined type. + $setting_key = "{$mode}_{$type}_key"; + $api_key = $this->get_setting($setting_key, '', $settings); + + return $api_key; + } + + /** + * Helper to implement the gform_paystack_api_mode filter so the api mode can be overridden. + * + * @access public + * + * @used-by GFPaystack::get_api_key() + * @used-by GFPaystack::callback() + * @used-by GFPaystack::can_create_feed() + * + * @param array $settings The plugin settings. + * @param int $feed_id Feed ID. + * + * @return string $api_mode Either live or test. + */ + public function get_api_mode($settings, $feed_id = null) + { + // Get API mode from settings. + $api_mode = rgar($settings, 'api_mode'); + + /** + * Filters the API mode. + * + * @param string $api_mode The API mode. + * @param int $feed_id Feed ID. + */ + return apply_filters('gform_paystack_api_mode', $api_mode, $feed_id); + } + + /** + * Retrieve the specified api key from the query string. + * + * @access public + * + * @used-by GFPaystack::get_api_key() + * + * @param string $type The type of key to retrieve. Defaults to 'secret'. + * + * @return string The result of the query string. + */ + public function get_query_string_api_key($type) + { + return rgget($type); + } + + /** + * Retrieve the public api key. + * + * @access public + * + * @uses GFPaystack::get_api_key() + * + * @param null|string $mode The API mode; live or test. + * @param array|null $settings The current settings. + * + * @return string The public API key. + */ + public function get_public_api_key($mode = null, $settings = null) + { + if (empty($settings)) { + $settings = $this->get_plugin_settings(); + } + + if (empty($mode)) { + $mode = $this->get_api_mode($settings); + } + + return $this->get_api_key('public', $mode, $settings); + } + + /** + * Retrieve the secret api key. + * + * @access public + * + * @uses GFPaystack::get_api_key() + * + * @param null|string $mode The API mode; live or test. + * @param null|array $settings The current settings. + * + * @return string The secret API key. + */ + public function get_secret_api_key($mode = null, $settings = null) + { + if (empty($settings)) { + $settings = $this->get_plugin_settings(); + } + + if (empty($mode)) { + $mode = $this->get_api_mode($settings); + } + + return $this->get_api_key('secret', $mode, $settings); + } + + /** + * Adds the currency if it isn't already registered. + * + * @since 1.0 + * @access public + * @used-by gform_currencies + * + * @param array $currencies The current currencies registered in Gravity Forms. + * + * @return array List of supported currencies. + */ public function add_paystack_currencies($currencies) { // Check if the currency is already registered. @@ -1927,10 +1927,10 @@ public function add_paystack_currencies($currencies) 'thousand_separator' => ',', 'decimal_separator' => '.', 'decimals' => 2, - 'code' => 'NGN' + 'code' => 'NGN' ); } - + // Check if the currency is already registered. if (!array_key_exists('GHS', $currencies)) { // Add GHS to the list of supported currencies. @@ -1942,10 +1942,10 @@ public function add_paystack_currencies($currencies) 'thousand_separator' => ',', 'decimal_separator' => '.', 'decimals' => 2, - 'code' => 'GHS' + 'code' => 'GHS' ); } - + // Check if the currency is already registered. if (!array_key_exists('ZAR', $currencies)) { // Add ZAR to the list of supported currencies. @@ -1957,10 +1957,9 @@ public function add_paystack_currencies($currencies) 'thousand_separator' => ',', 'decimal_separator' => '.', 'decimals' => 2, - 'code' => 'ZAR' + 'code' => 'ZAR' ); } - // Check if the currency is already registered. if (!array_key_exists('KES', $currencies)) { @@ -1973,12 +1972,12 @@ public function add_paystack_currencies($currencies) 'thousand_separator' => ',', 'decimal_separator' => '.', 'decimals' => 2, - 'code' => 'KES' + 'code' => 'KES' ); - - + } + // Check if the currency is already registered. - if (!array_key_exists('', $currencies)) { + if (!array_key_exists('XOF', $currencies)) { // Add XOF to the list of supported currencies. $currencies['XOF'] = array( 'name' => 'West African CFA franc', @@ -1988,12 +1987,12 @@ public function add_paystack_currencies($currencies) 'thousand_separator' => ',', 'decimal_separator' => '.', 'decimals' => 2, - 'code' => 'XOF' + 'code' => 'XOF' ); - - + } + // Check if the currency is already registered. - if (!array_key_exists('', $currencies)) { + if (!array_key_exists('EGP', $currencies)) { // Add EGP to the list of supported currencies. $currencies['EGP'] = array( 'name' => 'Egyptian Pound', @@ -2003,43 +2002,42 @@ public function add_paystack_currencies($currencies) 'thousand_separator' => ',', 'decimal_separator' => '.', 'decimals' => 2, - 'code' => 'EGP' + 'code' => 'EGP' ); } - + return $currencies; } - /** - * Check if rate limits is enabled. - * - * - * @param int $form_id The form ID. - * - * @return bool - */ - public function is_rate_limits_enabled($form_id) - { - /** - * Allow enabling or disable the rate limit check. - * - * @param bool $has_error The default is false. - * @param int $form_id The form ID. - */ - $this->_enable_rate_limits = apply_filters('gform_paystack_enable_rate_limits', $this->_enable_rate_limits, $form_id); - - return $this->_enable_rate_limits; - } - - public function get_entry_id_by_reference($reference) - { - global $wpdb; - - $entry_meta_table_name = self::get_entry_meta_table_name(); - - $sql = $wpdb->prepare("SELECT entry_id FROM {$entry_meta_table_name} WHERE meta_key = 'paystack_tx_reference' AND meta_value = '%s'", $reference); - $entry_id = $wpdb->get_var($sql); - - return $entry_id ? $entry_id : false; - } + /** + * Check if rate limits is enabled. + * + * @param int $form_id The form ID. + * + * @return bool + */ + public function is_rate_limits_enabled($form_id) + { + /** + * Allow enabling or disable the rate limit check. + * + * @param bool $has_error The default is false. + * @param int $form_id The form ID. + */ + $this->_enable_rate_limits = apply_filters('gform_paystack_enable_rate_limits', $this->_enable_rate_limits, $form_id); + + return $this->_enable_rate_limits; + } + + public function get_entry_id_by_reference($reference) + { + global $wpdb; + + $entry_meta_table_name = self::get_entry_meta_table_name(); + + $sql = $wpdb->prepare("SELECT entry_id FROM {$entry_meta_table_name} WHERE meta_key = 'paystack_tx_reference' AND meta_value = '%s'", $reference); + $entry_id = $wpdb->get_var($sql); + + return $entry_id ? $entry_id : false; + } }