관리-도구
편집 파일: class-kkart-gateway-payfast.php
<?php /** * PayFast Payment Gateway * * Provides a PayFast Payment Gateway. * * @class KKART_Gateway_Payfast * @package Kkart * @version 1.0.0 * @category Payment Gateways * @author Kkart */ defined( 'ABSPATH' ) || exit; define( 'KKART_GATEWAY_PAYFAST_VERSION', KKART_VERSION ); define( 'KKART_GATEWAY_PAYFAST_URL', untrailingslashit( plugin_dir_url(__FILE__) ) ); define( 'KKART_GATEWAY_PAYFAST_PATH', untrailingslashit( plugin_dir_path( __FILE__ ) ) ); class KKART_Gateway_PayFast extends KKART_Payment_Gateway { /** * Version * * @var string */ public $version; /** * @access protected * @var array $data_to_send */ protected $data_to_send = array(); /** * Constructor */ public function __construct() { $this->version = KKART_GATEWAY_PAYFAST_VERSION; $this->id = 'payfast'; $this->method_title = __( 'PayFast', 'kkart' ); /* translators: 1: a href link 2: closing href */ $this->method_description = sprintf( __( 'PayFast works by sending the user to %1$sPayFast%2$s to enter their payment information.', 'kkart' ), '<a href="http://payfast.co.za/">', '</a>' ); $this->icon = KKART_GATEWAY_PAYFAST_URL . '/assets/images/icon.png'; $this->debug_email = get_option( 'admin_email' ); $this->available_countries = array( 'ZA' ); $this->available_currencies = (array)apply_filters('kkart_gateway_payfast_available_currencies', array( 'ZAR' ) ); // Supported functionality $this->supports = array( 'products', 'pre-orders', 'subscriptions', 'subscription_cancellation', 'subscription_suspension', 'subscription_reactivation', 'subscription_amount_changes', 'subscription_date_changes', 'subscription_payment_method_change', // Subs 1.x support //'subscription_payment_method_change_customer', // see issue #39 ); $this->init_form_fields(); $this->init_settings(); if ( ! is_admin() ) { $this->setup_constants(); } // Setup default merchant data. $this->merchant_id = $this->get_option( 'merchant_id' ); $this->merchant_key = $this->get_option( 'merchant_key' ); $this->pass_phrase = $this->get_option( 'pass_phrase' ); $this->url = 'https://www.payfast.co.za/eng/process'; $this->validate_url = 'https://www.payfast.co.za/eng/query/validate'; $this->title = $this->get_option( 'title' ); $this->response_url = add_query_arg( 'kkart-api', 'KKART_Gateway_PayFast', home_url( '/' ) ); $this->send_debug_email = 'yes' === $this->get_option( 'send_debug_email' ); $this->description = $this->get_option( 'description' ); $this->enabled = 'yes' === $this->get_option( 'enabled' ) ? 'yes' : 'no'; $this->enable_logging = 'yes' === $this->get_option( 'enable_logging' ); // Setup the test data, if in test mode. if ( 'yes' === $this->get_option( 'testmode' ) ) { $this->url = 'https://sandbox.payfast.co.za/eng/process'; $this->validate_url = 'https://sandbox.payfast.co.za/eng/query/validate'; $this->add_testmode_admin_settings_notice(); } else { $this->send_debug_email = false; } add_action( 'kkart_api_kkart_gateway_payfast', array( $this, 'check_itn_response' ) ); add_action( 'kkart_update_options_payment_gateways_' . $this->id, array( $this, 'process_admin_options' ) ); add_action( 'kkart_receipt_'.$this->id, array( $this, 'receipt_page' ), 10, 1 ); add_action( 'kkart_scheduled_subscription_payment_' . $this->id, array( $this, 'scheduled_subscription_payment' ), 10, 2 ); add_action( 'kkart_subscription_status_cancelled', array( $this, 'cancel_subscription_listener' ) ); add_action( 'kkart_pre_orders_process_pre_order_completion_payment_' . $this->id, array( $this, 'process_pre_order_payments' ) ); add_action( 'admin_notices', array( $this, 'admin_notices' ) ); //Add fees to order add_action( 'kkart_admin_order_totals_after_total', array( $this, 'display_order_fee') ); add_action( 'kkart_admin_order_totals_after_total', array( $this, 'display_order_net'), 20 ); } /** * Initialise Gateway Settings Form Fields * * @since 1.0.0 */ public function init_form_fields() { $this->form_fields = array( 'enabled' => array( 'title' => __( 'Enable/Disable', 'kkart' ), 'label' => __( 'Enable PayFast', 'kkart' ), 'type' => 'checkbox', 'description' => __( 'This controls whether or not this gateway is enabled within Kkart.', 'kkart' ), 'default' => 'no', // User should enter the required information before enabling the gateway. 'desc_tip' => true, ), 'title' => array( 'title' => __( 'Title', 'kkart' ), 'type' => 'text', 'description' => __( 'This controls the title which the user sees during checkout.', 'kkart' ), 'default' => __( 'PayFast', 'kkart' ), 'desc_tip' => true, ), 'description' => array( 'title' => __( 'Description', 'kkart' ), 'type' => 'text', 'description' => __( 'This controls the description which the user sees during checkout.', 'kkart' ), 'default' => '', 'desc_tip' => true, ), 'testmode' => array( 'title' => __( 'PayFast Sandbox', 'kkart' ), 'type' => 'checkbox', 'description' => __( 'Place the payment gateway in development mode.', 'kkart' ), 'default' => 'yes', ), 'merchant_id' => array( 'title' => __( 'Merchant ID', 'kkart' ), 'type' => 'text', 'description' => __( 'This is the merchant ID, received from PayFast.', 'kkart' ), 'default' => '', ), 'merchant_key' => array( 'title' => __( 'Merchant Key', 'kkart' ), 'type' => 'text', 'description' => __( 'This is the merchant key, received from PayFast.', 'kkart' ), 'default' => '', ), 'pass_phrase' => array( 'title' => __( 'Passphrase', 'kkart' ), 'type' => 'text', 'description' => __( '* Required. Needed to ensure the data passed through is secure.', 'kkart' ), 'default' => '', ), 'send_debug_email' => array( 'title' => __( 'Send Debug Emails', 'kkart' ), 'type' => 'checkbox', 'label' => __( 'Send debug e-mails for transactions through the PayFast gateway (sends on successful transaction as well).', 'kkart' ), 'default' => 'yes', ), 'debug_email' => array( 'title' => __( 'Who Receives Debug E-mails?', 'kkart' ), 'type' => 'text', 'description' => __( 'The e-mail address to which debugging error e-mails are sent when in test mode.', 'kkart' ), 'default' => get_option( 'admin_email' ), ), 'enable_logging' => array( 'title' => __( 'Enable Logging', 'kkart' ), 'type' => 'checkbox', 'label' => __( 'Enable transaction logging for gateway.', 'kkart' ), 'default' => 'no', ), ); } /** * add_testmode_admin_settings_notice() * Add a notice to the merchant_key and merchant_id fields when in test mode. * * @since 1.0.0 */ public function add_testmode_admin_settings_notice() { $this->form_fields['merchant_id']['description'] .= ' <strong>' . __( 'Sandbox Merchant ID currently in use', 'kkart' ) . ' ( ' . esc_html( $this->merchant_id ) . ' ).</strong>'; $this->form_fields['merchant_key']['description'] .= ' <strong>' . __( 'Sandbox Merchant Key currently in use', 'kkart' ) . ' ( ' . esc_html( $this->merchant_key ) . ' ).</strong>'; } /** * check_requirements() * * Check if this gateway is enabled and available in the base currency being traded with. * * @since 1.0.0 * @return array */ public function check_requirements() { $errors = [ // Check if the store currency is supported by PayFast ! in_array( get_kkart_currency(), $this->available_currencies ) ? 'kkart-gateway-payfast-error-invalid-currency' : null, // Check if user entered the merchant ID empty( $this->get_option( 'merchant_id' ) ) ? 'kkart-gateway-payfast-error-missing-merchant-id' : null, // Check if user entered the merchant key empty( $this->get_option( 'merchant_key' ) ) ? 'kkart-gateway-payfast-error-missing-merchant-key' : null, // Check if user entered a pass phrase empty( $this->get_option( 'pass_phrase' ) ) ? 'kkart-gateway-payfast-error-missing-pass-phrase' : null ]; return array_filter( $errors ); } /** * Check if the gateway is available for use. * * @return bool */ public function is_available() { if ( 'yes' === $this->enabled ) { $errors = $this->check_requirements(); // Prevent using this gateway on frontend if there are any configuration errors. return 0 === count( $errors ); } return parent::is_available(); } /** * Admin Panel Options * - Options for bits like 'title' and availability on a country-by-country basis * * @since 1.0.0 */ public function admin_options() { if ( in_array( get_kkart_currency(), $this->available_currencies ) ) { parent::admin_options(); } else { ?> <h3><?php _e( 'PayFast', 'kkart' ); ?></h3> <div class="inline error"><p><strong><?php _e( 'Gateway Disabled:', 'kkart' ); ?></strong> <?php /* translators: 1: a href link 2: closing href */ echo sprintf( __( 'PayFast only supports South African Rands as their payment currency so Choose South African Rands as your store currency in %1$sGeneral Settings%2$s to enable the PayFast Gateway. For more information %3$s.', 'kkart' ), '<a href="' . esc_url( admin_url( 'admin.php?page=kkart-settings&tab=general' ) ) . '">', '</a>', '<a href="https://support.payfast.co.za/portal/en/kb/articles/can-i-receive-payments-in-usd-eur-gbp-etc" target="_blank">Click Here</a>' ); ?></p></div> <?php } } /** * Generate the PayFast button link. * * @since 1.0.0 */ public function generate_payfast_form( $order_id ) { $order = kkart_get_order( $order_id ); // Construct variables for post $this->data_to_send = array( // Merchant details 'merchant_id' => $this->merchant_id, 'merchant_key' => $this->merchant_key, 'return_url' => $this->get_return_url( $order ), 'cancel_url' => $order->get_cancel_order_url(), 'notify_url' => $this->response_url, // Billing details 'name_first' => self::get_order_prop( $order, 'billing_first_name' ), 'name_last' => self::get_order_prop( $order, 'billing_last_name' ), 'email_address' => self::get_order_prop( $order, 'billing_email' ), // Item details 'm_payment_id' => ltrim( $order->get_order_number(), _x( '#', 'hash before order number', 'kkart' ) ), 'amount' => $order->get_total(), 'item_name' => get_bloginfo( 'name' ) . ' - ' . $order->get_order_number(), /* translators: 1: blog info name */ 'item_description' => sprintf( __( 'New order from %s', 'kkart' ), get_bloginfo( 'name' ) ), // Custom strings 'custom_str1' => self::get_order_prop( $order, 'order_key' ), 'custom_str2' => 'Kkart/' . KKART_VERSION . '; ' . get_site_url(), 'custom_str3' => self::get_order_prop( $order, 'id' ), 'source' => 'Kkart-Free-Plugin', ); // add subscription parameters if ( $this->order_contains_subscription( $order_id ) ) { // 2 == ad-hoc subscription type see PayFast API docs $this->data_to_send['subscription_type'] = '2'; } if ( function_exists( 'kkarts_order_contains_renewal' ) && kkarts_order_contains_renewal( $order ) ) { $subscriptions = kkarts_get_subscriptions_for_renewal_order( $order_id ); // For renewal orders that have subscriptions with renewal flag, // we will create a new subscription in PayFast and link it to the existing ones in KKART. // The old subscriptions in PayFast will be cancelled once we handle the itn request. if ( count ( $subscriptions ) > 0 && $this->_has_renewal_flag( reset( $subscriptions ) ) ) { // 2 == ad-hoc subscription type see PayFast API docs $this->data_to_send['subscription_type'] = '2'; } } // pre-order: add the subscription type for pre order that require tokenization // at this point we assume that the order pre order fee and that // we should only charge that on the order. The rest will be charged later. if ( $this->order_contains_pre_order( $order_id ) && $this->order_requires_payment_tokenization( $order_id ) ) { $this->data_to_send['amount'] = $this->get_pre_order_fee( $order_id ); $this->data_to_send['subscription_type'] = '2'; } $payfast_args_array = array(); $sign_strings = []; foreach ( $this->data_to_send as $key => $value ) { if ($key !== 'source') { $sign_strings[] = esc_attr( $key ) . '=' . urlencode(str_replace('&', '&', trim( $value ))); } $payfast_args_array[] = '<input type="hidden" name="' . esc_attr( $key ) . '" value="' . esc_attr( $value ) . '" />'; } if (!empty($this->pass_phrase)) { $payfast_args_array[] = '<input type="hidden" name="signature" value="' . md5(implode('&', $sign_strings) . '&passphrase=' . urlencode($this->pass_phrase)) . '" />'; } else { $payfast_args_array[] = '<input type="hidden" name="signature" value="' . md5(implode('&', $sign_strings)) . '" />'; } return '<form action="' . esc_url( $this->url ) . '" method="post" id="payfast_payment_form"> ' . implode( '', $payfast_args_array ) . ' <input type="submit" class="button-alt" id="submit_payfast_payment_form" value="' . __( 'Pay via PayFast', 'kkart' ) . '" /> <a class="button cancel" href="' . $order->get_cancel_order_url() . '">' . __( 'Cancel order & restore cart', 'kkart' ) . '</a> <script type="text/javascript"> jQuery(function(){ jQuery("body").block( { message: "' . __( 'Thank you for your order. We are now redirecting you to PayFast to make payment.', 'kkart' ) . '", overlayCSS: { background: "#fff", opacity: 0.6 }, css: { padding: 20, textAlign: "center", color: "#555", border: "3px solid #aaa", backgroundColor:"#fff", cursor: "wait" } }); jQuery( "#submit_payfast_payment_form" ).click(); }); </script> </form>'; } /** * Process the payment and return the result. * * @since 1.0.0 */ public function process_payment( $order_id ) { if ( $this->order_contains_pre_order( $order_id ) && $this->order_requires_payment_tokenization( $order_id ) && ! $this->cart_contains_pre_order_fee() ) { throw new Exception( 'PayFast does not support transactions without any upfront costs or fees. Please select another gateway' ); } $order = kkart_get_order( $order_id ); return array( 'result' => 'success', 'redirect' => $order->get_checkout_payment_url( true ), ); } /** * Reciept page. * * Display text and a button to direct the user to PayFast. * * @since 1.0.0 */ public function receipt_page( $order ) { echo '<p>' . __( 'Thank you for your order, please click the button below to pay with PayFast.', 'kkart' ) . '</p>'; echo $this->generate_payfast_form( $order ); } /** * Check PayFast ITN response. * * @since 1.0.0 */ public function check_itn_response() { $this->handle_itn_request( stripslashes_deep( $_POST ) ); // Notify PayFast that information has been received header( 'HTTP/1.0 200 OK' ); flush(); } /** * Check PayFast ITN validity. * * @param array $data * @since 1.0.0 */ public function handle_itn_request( $data ) { $this->log( PHP_EOL . '----------' . PHP_EOL . 'PayFast ITN call received' . PHP_EOL . '----------' ); $this->log( 'Get posted data' ); $this->log( 'PayFast Data: ' . print_r( $data, true ) ); $payfast_error = false; $payfast_done = false; $debug_email = $this->get_option( 'debug_email', get_option( 'admin_email' ) ); $session_id = $data['custom_str1']; $vendor_name = get_bloginfo( 'name', 'display' ); $vendor_url = home_url( '/' ); $order_id = absint( $data['custom_str3'] ); $order_key = kkart_clean( $session_id ); $order = kkart_get_order( $order_id ); $original_order = $order; if ( false === $data ) { $payfast_error = true; $payfast_error_message = PF_ERR_BAD_ACCESS; } // Verify security signature if ( ! $payfast_error && ! $payfast_done ) { $this->log( 'Verify security signature' ); $signature = md5( $this->_generate_parameter_string( $data, false, false ) ); // false not to sort data // If signature different, log for debugging if ( ! $this->validate_signature( $data, $signature ) ) { $payfast_error = true; $payfast_error_message = PF_ERR_INVALID_SIGNATURE; } } // Verify source IP (If not in debug mode) if ( ! $payfast_error && ! $payfast_done && $this->get_option( 'testmode' ) != 'yes' ) { $this->log( 'Verify source IP' ); if ( ! $this->is_valid_ip( $_SERVER['REMOTE_ADDR'] ) ) { $payfast_error = true; $payfast_error_message = PF_ERR_BAD_SOURCE_IP; } } // Verify data received if ( ! $payfast_error ) { $this->log( 'Verify data received' ); $validation_data = $data; unset( $validation_data['signature'] ); $has_valid_response_data = $this->validate_response_data( $validation_data ); if ( ! $has_valid_response_data ) { $payfast_error = true; $payfast_error_message = PF_ERR_BAD_ACCESS; } } // Check data against internal order if ( ! $payfast_error && ! $payfast_done ) { $this->log( 'Check data against internal order' ); // Check order amount if ( ! $this->amounts_equal( $data['amount_gross'], self::get_order_prop( $order, 'order_total' ) ) && ! $this->order_contains_pre_order( $order_id ) && ! $this->order_contains_subscription( $order_id ) ) { $payfast_error = true; $payfast_error_message = PF_ERR_AMOUNT_MISMATCH; } elseif ( strcasecmp( $data['custom_str1'], self::get_order_prop( $order, 'order_key' ) ) != 0 ) { // Check session ID $payfast_error = true; $payfast_error_message = PF_ERR_SESSIONID_MISMATCH; } } // alter order object to be the renewal order if // the ITN request comes as a result of a renewal submission request $description = json_decode( $data['item_description'] ); if ( ! empty( $description->renewal_order_id ) ) { $order = kkart_get_order( $description->renewal_order_id ); } // Get internal order and verify it hasn't already been processed if ( ! $payfast_error && ! $payfast_done ) { $this->log_order_details( $order ); // Check if order has already been processed if ( 'completed' === self::get_order_prop( $order, 'status' ) ) { $this->log( 'Order has already been processed' ); $payfast_done = true; } } // If an error occurred if ( $payfast_error ) { $this->log( 'Error occurred: ' . $payfast_error_message ); if ( $this->send_debug_email ) { $this->log( 'Sending email notification' ); // Send an email $subject = 'PayFast ITN error: ' . $payfast_error_message; $body = "Hi,\n\n" . "An invalid PayFast transaction on your website requires attention\n" . "------------------------------------------------------------\n" . 'Site: ' . esc_html( $vendor_name ) . ' (' . esc_url( $vendor_url ) . ")\n" . 'Remote IP Address: ' . $_SERVER['REMOTE_ADDR'] . "\n" . 'Remote host name: ' . gethostbyaddr( $_SERVER['REMOTE_ADDR'] ) . "\n" . 'Purchase ID: ' . self::get_order_prop( $order, 'id' ) . "\n" . 'User ID: ' . self::get_order_prop( $order, 'user_id' ) . "\n"; if ( isset( $data['pf_payment_id'] ) ) { $body .= 'PayFast Transaction ID: ' . esc_html( $data['pf_payment_id'] ) . "\n"; } if ( isset( $data['payment_status'] ) ) { $body .= 'PayFast Payment Status: ' . esc_html( $data['payment_status'] ) . "\n"; } $body .= "\nError: " . $payfast_error_message . "\n"; switch ( $payfast_error_message ) { case PF_ERR_AMOUNT_MISMATCH: $body .= 'Value received : ' . esc_html( $data['amount_gross'] ) . "\n" . 'Value should be: ' . self::get_order_prop( $order, 'order_total' ); break; case PF_ERR_ORDER_ID_MISMATCH: $body .= 'Value received : ' . esc_html( $data['custom_str3'] ) . "\n" . 'Value should be: ' . self::get_order_prop( $order, 'id' ); break; case PF_ERR_SESSIONID_MISMATCH: $body .= 'Value received : ' . esc_html( $data['custom_str1'] ) . "\n" . 'Value should be: ' . self::get_order_prop( $order, 'id' ); break; // For all other errors there is no need to add additional information default: break; } wp_mail( $debug_email, $subject, $body ); } // End if(). } elseif ( ! $payfast_done ) { $this->log( 'Check status and update order' ); if ( self::get_order_prop( $original_order, 'order_key' ) !== $order_key ) { $this->log( 'Order key does not match' ); exit; } $status = strtolower( $data['payment_status'] ); $subscriptions = array(); if ( function_exists( 'kkarts_get_subscriptions_for_renewal_order' ) && function_exists( 'kkarts_get_subscriptions_for_order' ) ) { $subscriptions = array_merge( kkarts_get_subscriptions_for_renewal_order( $order_id ), kkarts_get_subscriptions_for_order( $order_id ) ); } if ( 'complete' !== $status && 'cancelled' !== $status ) { foreach ( $subscriptions as $subscription ) { $this->_set_renewal_flag( $subscription ); } } if ( 'complete' === $status ) { $this->handle_itn_payment_complete( $data, $order, $subscriptions ); } elseif ( 'failed' === $status ) { $this->handle_itn_payment_failed( $data, $order ); } elseif ( 'pending' === $status ) { $this->handle_itn_payment_pending( $data, $order ); } elseif ( 'cancelled' === $status ) { $this->handle_itn_payment_cancelled( $data, $order, $subscriptions ); } } // End if(). $this->log( PHP_EOL . '----------' . PHP_EOL . 'End ITN call' . PHP_EOL . '----------' ); } /** * Handle logging the order details. * * @since 1.4.5 */ public function log_order_details( $order ) { if ( version_compare( KKART_VERSION,'3.0.0', '<' ) ) { $customer_id = get_post_meta( $order->get_id(), '_customer_user', true ); } else { $customer_id = $order->get_user_id(); } $details = "Order Details:" . PHP_EOL . 'customer id:' . $customer_id . PHP_EOL . 'order id: ' . $order->get_id() . PHP_EOL . 'parent id: ' . $order->get_parent_id() . PHP_EOL . 'status: ' . $order->get_status() . PHP_EOL . 'total: ' . $order->get_total() . PHP_EOL . 'currency: ' . $order->get_currency() . PHP_EOL . 'key: ' . $order->get_order_key() . ""; $this->log( $details ); } /** * This function mainly responds to ITN cancel requests initiated on PayFast, but also acts * just in case they are not cancelled. * @version 1.4.3 Subscriptions flag * * @param array $data should be from the Gatewy ITN callback. * @param KKART_Order $order */ public function handle_itn_payment_cancelled( $data, $order, $subscriptions ) { remove_action( 'kkart_subscription_status_cancelled', array( $this, 'cancel_subscription_listener' ) ); foreach ( $subscriptions as $subscription ) { if ( 'cancelled' !== $subscription->get_status() ) { $subscription->update_status( 'cancelled', __( 'Merchant cancelled subscription on PayFast.' , 'kkart' ) ); $this->_delete_subscription_token( $subscription ); } } add_action( 'kkart_subscription_status_cancelled', array( $this, 'cancel_subscription_listener' ) ); } /** * This function handles payment complete request by PayFast. * @version 1.4.3 Subscriptions flag * * @param array $data should be from the Gatewy ITN callback. * @param KKART_Order $order */ public function handle_itn_payment_complete( $data, $order, $subscriptions ) { $this->log( '- Complete' ); $order->add_order_note( __( 'ITN payment completed', 'kkart' ) ); $order->update_meta_data( 'payfast_amount_fee', $data['amount_fee'] ); $order->update_meta_data( 'payfast_amount_net', $data['amount_net'] ); $order_id = self::get_order_prop( $order, 'id' ); // Store token for future subscription deductions. if ( count( $subscriptions ) > 0 && isset( $data['token'] ) ) { if ( $this->_has_renewal_flag( reset( $subscriptions ) ) ) { // renewal flag is set to true, so we need to cancel previous token since we will create a new one $this->log( 'Cancel previous subscriptions with token ' . $this->_get_subscription_token( reset( $subscriptions ) ) ); // only request API cancel token for the first subscription since all of them are using the same token $this->cancel_subscription_listener( reset( $subscriptions ) ); } $token = sanitize_text_field( $data['token'] ); foreach ( $subscriptions as $subscription ) { $this->_delete_renewal_flag( $subscription ); $this->_set_subscription_token( $token, $subscription ); } } // the same mechanism (adhoc token) is used to capture payment later if ( $this->order_contains_pre_order( $order_id ) && $this->order_requires_payment_tokenization( $order_id ) ) { $token = sanitize_text_field( $data['token'] ); $is_pre_order_fee_paid = get_post_meta( $order_id, '_pre_order_fee_paid', true ) === 'yes'; if ( ! $is_pre_order_fee_paid ) { /* translators: 1: gross amount 2: payment id */ $order->add_order_note( sprintf( __( 'PayFast pre-order fee paid: R %1$s (%2$s)', 'kkart' ), $data['amount_gross'], $data['pf_payment_id'] ) ); $this->_set_pre_order_token( $token, $order ); // set order to pre-ordered KKART_Pre_Orders_Order::mark_order_as_pre_ordered( $order ); update_post_meta( $order_id, '_pre_order_fee_paid', 'yes' ); KKART()->cart->empty_cart(); } else { /* translators: 1: gross amount 2: payment id */ $order->add_order_note( sprintf( __( 'PayFast pre-order product line total paid: R %1$s (%2$s)', 'kkart' ), $data['amount_gross'], $data['pf_payment_id'] ) ); $order->payment_complete(); $this->cancel_pre_order_subscription( $token ); } } else { $order->payment_complete(); } $debug_email = $this->get_option( 'debug_email', get_option( 'admin_email' ) ); $vendor_name = get_bloginfo( 'name', 'display' ); $vendor_url = home_url( '/' ); if ( $this->send_debug_email ) { $subject = 'PayFast ITN on your site'; $body = "Hi,\n\n" . "A PayFast transaction has been completed on your website\n" . "------------------------------------------------------------\n" . 'Site: ' . esc_html( $vendor_name ) . ' (' . esc_url( $vendor_url ) . ")\n" . 'Purchase ID: ' . esc_html( $data['m_payment_id'] ) . "\n" . 'PayFast Transaction ID: ' . esc_html( $data['pf_payment_id'] ) . "\n" . 'PayFast Payment Status: ' . esc_html( $data['payment_status'] ) . "\n" . 'Order Status Code: ' . self::get_order_prop( $order, 'status' ); wp_mail( $debug_email, $subject, $body ); } } /** * @param $data * @param $order */ public function handle_itn_payment_failed( $data, $order ) { $this->log( '- Failed' ); /* translators: 1: payment status */ $order->update_status( 'failed', sprintf( __( 'Payment %s via ITN.', 'kkart' ), strtolower( sanitize_text_field( $data['payment_status'] ) ) ) ); $debug_email = $this->get_option( 'debug_email', get_option( 'admin_email' ) ); $vendor_name = get_bloginfo( 'name', 'display' ); $vendor_url = home_url( '/' ); if ( $this->send_debug_email ) { $subject = 'PayFast ITN Transaction on your site'; $body = "Hi,\n\n" . "A failed PayFast transaction on your website requires attention\n" . "------------------------------------------------------------\n" . 'Site: ' . esc_html( $vendor_name ) . ' (' . esc_url( $vendor_url ) . ")\n" . 'Purchase ID: ' . self::get_order_prop( $order, 'id' ) . "\n" . 'User ID: ' . self::get_order_prop( $order, 'user_id' ) . "\n" . 'PayFast Transaction ID: ' . esc_html( $data['pf_payment_id'] ) . "\n" . 'PayFast Payment Status: ' . esc_html( $data['payment_status'] ); wp_mail( $debug_email, $subject, $body ); } } /** * @since 1.4.0 introduced * @param $data * @param $order */ public function handle_itn_payment_pending( $data, $order ) { $this->log( '- Pending' ); // Need to wait for "Completed" before processing /* translators: 1: payment status */ $order->update_status( 'on-hold', sprintf( __( 'Payment %s via ITN.', 'kkart' ), strtolower( sanitize_text_field( $data['payment_status'] ) ) ) ); } /** * @param string $order_id * @return double */ public function get_pre_order_fee( $order_id ) { foreach ( kkart_get_order( $order_id )->get_fees() as $fee ) { if ( is_array( $fee ) && 'Pre-Order Fee' == $fee['name'] ) { return doubleval( $fee['line_total'] ) + doubleval( $fee['line_tax'] ); } } } /** * @param string $order_id * @return bool */ public function order_contains_pre_order( $order_id ) { if ( class_exists( 'KKART_Pre_Orders_Order' ) ) { return KKART_Pre_Orders_Order::order_contains_pre_order( $order_id ); } return false; } /** * @param string $order_id * * @return bool */ public function order_requires_payment_tokenization( $order_id ) { if ( class_exists( 'KKART_Pre_Orders_Order' ) ) { return KKART_Pre_Orders_Order::order_requires_payment_tokenization( $order_id ); } return false; } /** * @return bool */ public function cart_contains_pre_order_fee() { if ( class_exists( 'KKART_Pre_Orders_Cart' ) ) { return KKART_Pre_Orders_Cart::cart_contains_pre_order_fee(); } return false; } /** * Store the PayFast subscription token * * @param string $token * @param KKART_Subscription $subscription */ protected function _set_subscription_token( $token, $subscription ) { update_post_meta( self::get_order_prop( $subscription, 'id' ), '_payfast_subscription_token', $token ); } /** * Retrieve the PayFast subscription token for a given order id. * * @param KKART_Subscription $subscription * @return mixed */ protected function _get_subscription_token( $subscription ) { return get_post_meta( self::get_order_prop( $subscription, 'id' ), '_payfast_subscription_token', true ); } /** * Retrieve the PayFast subscription token for a given order id. * * @param KKART_Subscription $subscription * @return mixed */ protected function _delete_subscription_token( $subscription ) { return delete_post_meta( self::get_order_prop( $subscription, 'id' ), '_payfast_subscription_token' ); } /** * Store the PayFast renewal flag * @since 1.4.3 * * @param string $token * @param KKART_Subscription $subscription */ protected function _set_renewal_flag( $subscription ) { if ( version_compare( KKART_VERSION, '3.0', '<' ) ) { update_post_meta( self::get_order_prop( $subscription, 'id' ), '_payfast_renewal_flag', 'true' ); } else { $subscription->update_meta_data( '_payfast_renewal_flag', 'true' ); $subscription->save_meta_data(); } } /** * Retrieve the PayFast renewal flag for a given order id. * @since 1.4.3 * * @param KKART_Subscription $subscription * @return bool */ protected function _has_renewal_flag( $subscription ) { if ( version_compare( KKART_VERSION, '3.0', '<' ) ) { return 'true' === get_post_meta( self::get_order_prop( $subscription, 'id' ), '_payfast_renewal_flag', true ); } else { return 'true' === $subscription->get_meta( '_payfast_renewal_flag', true ); } } /** * Retrieve the PayFast renewal flag for a given order id. * @since 1.4.3 * * @param KKART_Subscription $subscription * @return mixed */ protected function _delete_renewal_flag( $subscription ) { if ( version_compare( KKART_VERSION, '3.0', '<' ) ) { return delete_post_meta( self::get_order_prop( $subscription, 'id' ), '_payfast_renewal_flag' ); } else { $subscription->delete_meta_data( '_payfast_renewal_flag' ); $subscription->save_meta_data(); } } /** * Store the PayFast pre_order_token token * * @param string $token * @param KKART_Order $order */ protected function _set_pre_order_token( $token, $order ) { update_post_meta( self::get_order_prop( $order, 'id' ), '_payfast_pre_order_token', $token ); } /** * Retrieve the PayFast pre-order token for a given order id. * * @param KKART_Order $order * @return mixed */ protected function _get_pre_order_token( $order ) { return get_post_meta( self::get_order_prop( $order, 'id' ), '_payfast_pre_order_token', true ); } /** * Wrapper function for kkarts_order_contains_subscription * * @param KKART_Order $order * @return bool */ public function order_contains_subscription( $order ) { if ( ! function_exists( 'kkarts_order_contains_subscription' ) ) { return false; } return kkarts_order_contains_subscription( $order ); } /** * @param $amount_to_charge * @param KKART_Order $renewal_order */ public function scheduled_subscription_payment( $amount_to_charge, $renewal_order ) { $subscription = kkarts_get_subscription( get_post_meta( self::get_order_prop( $renewal_order, 'id' ), '_subscription_renewal', true ) ); $this->log( 'Attempting to renew subscription from renewal order ' . self::get_order_prop( $renewal_order, 'id' ) ); if ( empty( $subscription ) ) { $this->log( 'Subscription from renewal order was not found.' ); return; } $response = $this->submit_subscription_payment( $subscription, $amount_to_charge ); if ( is_wp_error( $response ) ) { /* translators: 1: error code 2: error message */ $renewal_order->update_status( 'failed', sprintf( __( 'PayFast Subscription renewal transaction failed (%1$s:%2$s)', 'kkart' ), $response->get_error_code() ,$response->get_error_message() ) ); } // Payment will be completion will be capture only when the ITN callback is sent to $this->handle_itn_request(). $renewal_order->add_order_note( __( 'PayFast Subscription renewal transaction submitted.', 'kkart' ) ); } /** * @param KKART_Subscription $subscription * @param $amount_to_charge * @return mixed WP_Error on failure, bool true on success */ public function submit_subscription_payment( $subscription, $amount_to_charge ) { $token = $this->_get_subscription_token( $subscription ); $item_name = $this->get_subscription_name( $subscription ); foreach ( $subscription->get_related_orders( 'all', 'renewal' ) as $order ) { $statuses_to_charge = array( 'on-hold', 'failed', 'pending' ); if ( in_array( $order->get_status(), $statuses_to_charge ) ) { $latest_order_to_renew = $order; break; } } $item_description = json_encode( array( 'renewal_order_id' => self::get_order_prop( $latest_order_to_renew, 'id' ) ) ); return $this->submit_ad_hoc_payment( $token, $amount_to_charge, $item_name, $item_description ); } /** * Get a name for the subscription item. For multiple * item only Subscription $date will be returned. * * For subscriptions with no items Site/Blog name will be returned. * * @param KKART_Subscription $subscription * @return string */ public function get_subscription_name( $subscription ) { if ( $subscription->get_item_count() > 1 ) { return $subscription->get_date_to_display( 'start' ); } else { $items = $subscription->get_items(); if ( empty( $items ) ) { return get_bloginfo( 'name' ); } $item = array_shift( $items ); return $item['name']; } } /** * Setup api data for the the adhoc payment. * * @since 1.4.0 introduced. * @param string $token * @param double $amount_to_charge * @param string $item_name * @param string $item_description * * @return bool|WP_Error */ public function submit_ad_hoc_payment( $token, $amount_to_charge, $item_name, $item_description ) { $args = array( 'body' => array( 'amount' => $amount_to_charge * 100, // convert to cents 'item_name' => $item_name, 'item_description' => $item_description, ), ); return $this->api_request( 'adhoc', $token, $args ); } /** * Send off API request. * * @since 1.4.0 introduced. * * @param $command * @param $token * @param $api_args * @param string $method GET | PUT | POST | DELETE. * * @return bool|WP_Error */ public function api_request( $command, $token, $api_args, $method = 'POST' ) { if ( empty( $token ) ) { $this->log( "Error posting API request: No token supplied", true ); return new WP_Error( '404', __( 'Can not submit PayFast request with an empty token', 'kkart' ), $results ); } $api_endpoint = "https://api.payfast.co.za/subscriptions/$token/$command"; $api_endpoint .= 'yes' === $this->get_option( 'testmode' ) ? '?testing=true' : ''; $timestamp = current_time( rtrim( DateTime::ATOM, 'P' ) ) . '+02:00'; $api_args['timeout'] = 45; $api_args['headers'] = array( 'merchant-id' => $this->merchant_id, 'timestamp' => $timestamp, 'version' => 'v1', ); // generate signature $all_api_variables = array_merge( $api_args['headers'], (array) $api_args['body'] ); $api_args['headers']['signature'] = md5( $this->_generate_parameter_string( $all_api_variables ) ); $api_args['method'] = strtoupper( $method ); $results = wp_remote_request( $api_endpoint, $api_args ); // Check PayFast server response if ( 200 !== $results['response']['code'] ) { $this->log( "Error posting API request:\n" . print_r( $results['response'], true ) ); return new WP_Error( $results['response']['code'], json_decode( $results['body'] )->data->response, $results ); } // Check adhoc bank charge response $results_data = json_decode( $results['body'], true )['data']; if ( $command == 'adhoc' && 'true' !== $results_data['response'] ) { $this->log( "Error posting API request:\n" . print_r( $results_data , true ) ); $code = is_array( $results_data['response'] ) ? $results_data['response']['code'] : $results_data['response']; $message = is_array( $results_data['response'] ) ? $results_data['response']['reason'] : $results_data['message']; // Use trim here to display it properly e.g. on an order note, since PayFast can include CRLF in a message. return new WP_Error( $code, trim( $message ), $results ); } $maybe_json = json_decode( $results['body'], true ); if ( ! is_null( $maybe_json ) && isset( $maybe_json['status'] ) && 'failed' === $maybe_json['status'] ) { $this->log( "Error posting API request:\n" . print_r( $results['body'], true ) ); // Use trim here to display it properly e.g. on an order note, since PayFast can include CRLF in a message. return new WP_Error( $maybe_json['code'], trim( $maybe_json['data']['message'] ), $results['body'] ); } return true; } /** * Responds to Subscriptions extension cancellation event. * * @since 1.4.0 introduced. * @param KKART_Subscription $subscription */ public function cancel_subscription_listener( $subscription ) { $token = $this->_get_subscription_token( $subscription ); if ( empty( $token ) ) { return; } $this->api_request( 'cancel', $token, array(), 'PUT' ); } /** * @since 1.4.0 * @param string $token * * @return bool|WP_Error */ public function cancel_pre_order_subscription( $token ) { return $this->api_request( 'cancel', $token, array(), 'PUT' ); } /** * @since 1.4.0 introduced. * @param $api_data * @param bool $sort_data_before_merge? default true. * @param bool $skip_empty_values Should key value pairs be ignored when generating signature? Default true. * * @return string */ protected function _generate_parameter_string( $api_data, $sort_data_before_merge = true, $skip_empty_values = true ) { // if sorting is required the passphrase should be added in before sort. if ( ! empty( $this->pass_phrase ) && $sort_data_before_merge ) { $api_data['passphrase'] = $this->pass_phrase; } if ( $sort_data_before_merge ) { ksort( $api_data ); } // concatenate the array key value pairs. $parameter_string = ''; foreach ( $api_data as $key => $val ) { if ( $skip_empty_values && empty( $val ) ) { continue; } if ( 'signature' !== $key ) { $val = urlencode( $val ); $parameter_string .= "$key=$val&"; } } // when not sorting passphrase should be added to the end before md5 if ( $sort_data_before_merge ) { $parameter_string = rtrim( $parameter_string, '&' ); } elseif ( ! empty( $this->pass_phrase ) ) { $parameter_string .= 'passphrase=' . urlencode( $this->pass_phrase ); } else { $parameter_string = rtrim( $parameter_string, '&' ); } return $parameter_string; } /** * @since 1.4.0 introduced. * @param KKART_Order $order */ public function process_pre_order_payments( $order ) { // The total amount to charge is the the order's total. $total = $order->get_total() - $this->get_pre_order_fee( self::get_order_prop( $order, 'id' ) ); $token = $this->_get_pre_order_token( $order ); if ( ! $token ) { return; } // get the payment token and attempt to charge the transaction $item_name = 'pre-order'; $results = $this->submit_ad_hoc_payment( $token, $total, $item_name, '' ); if ( is_wp_error( $results ) ) { /* translators: 1: error code 2: error message */ $order->update_status( 'failed', sprintf( __( 'PayFast Pre-Order payment transaction failed (%1$s:%2$s)', 'kkart' ), $results->get_error_code() ,$results->get_error_message() ) ); return; } // Payment completion will be handled by ITN callback } /** * Setup constants. * * Setup common values and messages used by the PayFast gateway. * * @since 1.0.0 */ public function setup_constants() { // Create user agent string. define( 'PF_SOFTWARE_NAME', 'Kkart' ); define( 'PF_SOFTWARE_VER', KKART_VERSION ); define( 'PF_MODULE_NAME', 'Kkart-PayFast-Free' ); define( 'PF_MODULE_VER', $this->version ); // Features // - PHP $pf_features = 'PHP ' . phpversion() . ';'; // - cURL if ( in_array( 'curl', get_loaded_extensions() ) ) { define( 'PF_CURL', '' ); $pf_version = curl_version(); $pf_features .= ' curl ' . $pf_version['version'] . ';'; } else { $pf_features .= ' nocurl;'; } // Create user agrent define( 'PF_USER_AGENT', PF_SOFTWARE_NAME . '/' . PF_SOFTWARE_VER . ' (' . trim( $pf_features ) . ') ' . PF_MODULE_NAME . '/' . PF_MODULE_VER ); // General Defines define( 'PF_TIMEOUT', 15 ); define( 'PF_EPSILON', 0.01 ); // Messages // Error define( 'PF_ERR_AMOUNT_MISMATCH', __( 'Amount mismatch', 'kkart' ) ); define( 'PF_ERR_BAD_ACCESS', __( 'Bad access of page', 'kkart' ) ); define( 'PF_ERR_BAD_SOURCE_IP', __( 'Bad source IP address', 'kkart' ) ); define( 'PF_ERR_CONNECT_FAILED', __( 'Failed to connect to PayFast', 'kkart' ) ); define( 'PF_ERR_INVALID_SIGNATURE', __( 'Security signature mismatch', 'kkart' ) ); define( 'PF_ERR_MERCHANT_ID_MISMATCH', __( 'Merchant ID mismatch', 'kkart' ) ); define( 'PF_ERR_NO_SESSION', __( 'No saved session found for ITN transaction', 'kkart' ) ); define( 'PF_ERR_ORDER_ID_MISSING_URL', __( 'Order ID not present in URL', 'kkart' ) ); define( 'PF_ERR_ORDER_ID_MISMATCH', __( 'Order ID mismatch', 'kkart' ) ); define( 'PF_ERR_ORDER_INVALID', __( 'This order ID is invalid', 'kkart' ) ); define( 'PF_ERR_ORDER_NUMBER_MISMATCH', __( 'Order Number mismatch', 'kkart' ) ); define( 'PF_ERR_ORDER_PROCESSED', __( 'This order has already been processed', 'kkart' ) ); define( 'PF_ERR_PDT_FAIL', __( 'PDT query failed', 'kkart' ) ); define( 'PF_ERR_PDT_TOKEN_MISSING', __( 'PDT token not present in URL', 'kkart' ) ); define( 'PF_ERR_SESSIONID_MISMATCH', __( 'Session ID mismatch', 'kkart' ) ); define( 'PF_ERR_UNKNOWN', __( 'Unkown error occurred', 'kkart' ) ); // General define( 'PF_MSG_OK', __( 'Payment was successful', 'kkart' ) ); define( 'PF_MSG_FAILED', __( 'Payment has failed', 'kkart' ) ); define( 'PF_MSG_PENDING', __( 'The payment is pending. Please note, you will receive another Instant Transaction Notification when the payment status changes to "Completed", or "Failed"', 'kkart' ) ); do_action( 'kkart_gateway_payfast_setup_constants' ); } /** * Log system processes. * @since 1.0.0 */ public function log( $message ) { if ( 'yes' === $this->get_option( 'testmode' ) || $this->enable_logging ) { if ( empty( $this->logger ) ) { $this->logger = new KKART_Logger(); } $this->logger->add( 'payfast', $message ); } } /** * validate_signature() * * Validate the signature against the returned data. * * @param array $data * @param string $signature * @since 1.0.0 * @return string */ public function validate_signature( $data, $signature ) { $result = $data['signature'] === $signature; $this->log( 'Signature = ' . ( $result ? 'valid' : 'invalid' ) ); return $result; } /** * Validate the IP address to make sure it's coming from PayFast. * * @param array $source_ip * @since 1.0.0 * @return bool */ public function is_valid_ip( $source_ip ) { // Variable initialization $valid_hosts = array( 'www.payfast.co.za', 'sandbox.payfast.co.za', 'w1w.payfast.co.za', 'w2w.payfast.co.za', ); $valid_ips = array(); foreach ( $valid_hosts as $pf_hostname ) { $ips = gethostbynamel( $pf_hostname ); if ( false !== $ips ) { $valid_ips = array_merge( $valid_ips, $ips ); } } // Remove duplicates $valid_ips = array_unique( $valid_ips ); // Adds support for X_Forwarded_For if ( ! empty( $_SERVER['HTTP_X_FORWARDED_FOR'] ) ) { $source_ip = (string) rest_is_ip_address( trim( current( preg_split( '/[,:]/', sanitize_text_field( wp_unslash( $_SERVER['HTTP_X_FORWARDED_FOR'] ) ) ) ) ) ) ?: $source_ip; } $this->log( "Valid IPs:\n" . print_r( $valid_ips, true ) ); $is_valid_ip = in_array( $source_ip, $valid_ips ); return apply_filters( 'kkart_gateway_payfast_is_valid_ip', $is_valid_ip, $source_ip ); } /** * validate_response_data() * * @param array $post_data * @param string $proxy Address of proxy to use or NULL if no proxy. * @since 1.0.0 * @return bool */ public function validate_response_data( $post_data, $proxy = null ) { $this->log( 'Host = ' . $this->validate_url ); $this->log( 'Params = ' . print_r( $post_data, true ) ); if ( ! is_array( $post_data ) ) { return false; } $response = wp_remote_post( $this->validate_url, array( 'body' => $post_data, 'timeout' => 70, 'user-agent' => PF_USER_AGENT, )); if ( is_wp_error( $response ) || empty( $response['body'] ) ) { $this->log( "Response error:\n" . print_r( $response, true ) ); return false; } parse_str( $response['body'], $parsed_response ); $response = $parsed_response; $this->log( "Response:\n" . print_r( $response, true ) ); // Interpret Response if ( is_array( $response ) && in_array( 'VALID', array_keys( $response ) ) ) { return true; } else { return false; } } /** * amounts_equal() * * Checks to see whether the given amounts are equal using a proper floating * point comparison with an Epsilon which ensures that insignificant decimal * places are ignored in the comparison. * * eg. 100.00 is equal to 100.0001 * * @author Jonathan Smit * @param $amount1 Float 1st amount for comparison * @param $amount2 Float 2nd amount for comparison * @since 1.0.0 * @return bool */ public function amounts_equal( $amount1, $amount2 ) { return ! ( abs( floatval( $amount1 ) - floatval( $amount2 ) ) > PF_EPSILON ); } /** * Get order property with compatibility check on order getter introduced * in KKART 3.0. * * @since 1.4.1 * * @param KKART_Order $order Order object. * @param string $prop Property name. * * @return mixed Property value */ public static function get_order_prop( $order, $prop ) { switch ( $prop ) { case 'order_total': $getter = array( $order, 'get_total' ); break; default: $getter = array( $order, 'get_' . $prop ); break; } return is_callable( $getter ) ? call_user_func( $getter ) : $order->{ $prop }; } /** * Gets user-friendly error message strings from keys * * @param string $key The key representing an error * * @return string The user-friendly error message for display */ public function get_error_message( $key ) { switch ( $key ) { case 'kkart-gateway-payfast-error-invalid-currency': return __( 'Your store uses a currency that PayFast doesnt support yet.', 'kkart' ); case 'kkart-gateway-payfast-error-missing-merchant-id': return __( 'You forgot to fill your merchant ID.', 'kkart' ); case 'kkart-gateway-payfast-error-missing-merchant-key': return __( 'You forgot to fill your merchant key.', 'kkart' ); case 'kkart-gateway-payfast-error-missing-pass-phrase': return __( 'PayFast requires a passphrase to work.', 'kkart' ); default: return ''; } } /** * Show possible admin notices */ public function admin_notices() { // Get requirement errors. $errors_to_show = $this->check_requirements(); // If everything is in place, don't display it. if ( ! count( $errors_to_show ) ) { return; } // If the gateway isn't enabled, don't show it. if ( "no" === $this->enabled ) { return; } // Use transients to display the admin notice once after saving values. if ( ! get_transient( 'kkart-gateway-payfast-admin-notice-transient' ) ) { set_transient( 'kkart-gateway-payfast-admin-notice-transient', 1, 1); echo '<div class="notice notice-error is-dismissible"><p>' . __( 'To use PayFast as a payment provider, you need to fix the problems below:', 'kkart' ) . '</p>' . '<ul style="list-style-type: disc; list-style-position: inside; padding-left: 2em;">' . array_reduce( $errors_to_show, function( $errors_list, $error_item ) { $errors_list = $errors_list . PHP_EOL . ( '<li>' . $this->get_error_message($error_item) . '</li>' ); return $errors_list; }, '' ) . '</ul></p></div>'; } } /** * Displays the amount_fee as returned by payfast. * * @param int $order_id The ID of the order. */ public function display_order_fee( $order_id ) { $order = kkart_get_order( $order_id ); $fee = get_post_meta( self::get_order_prop( $order, 'id' ), 'payfast_amount_fee', TRUE); if (! $fee ) { return; } ?> <tr> <td class="label payfast-fee"> <?php echo kkart_help_tip( __( 'This represents the fee Payfast collects for the transaction.', 'kkart' ) ); ?> <?php esc_html_e( 'Payfast Fee:', 'kkart' ); ?> </td> <td width="1%"></td> <td class="total"> <?php echo kkart_price( $fee, array( 'decimals' => 2 )); ?> </td> </tr> <?php } /** * Displays the amount_net as returned by payfast. * * @param int $order_id The ID of the order. */ public function display_order_net( $order_id ) { $order = kkart_get_order( $order_id ); $net = get_post_meta( self::get_order_prop( $order, 'id' ), 'payfast_amount_net', TRUE); if (! $net ) { return; } ?> <tr> <td class="label payfast-net"> <?php echo kkart_help_tip( __( 'This represents the net total that was credited to your Payfast account.', 'kkart' ) ); ?> <?php esc_html_e( 'Amount Net:', 'kkart' ); ?> </td> <td width="1%"></td> <td class="total"> <?php echo kkart_price( $net, array( 'decimals' => 2 ) ); ?> </td> </tr> <?php } }