id = 'arsenalpay'; $this->icon = apply_filters('woocommerce_arsenalpay_icon', '' . plugin_dir_url(__FILE__) . 'wc-arsenalpay.png'); $this->method_title = __('ArsenalPay', 'woocommerce'); $this->method_description = __('Allows payments with ArsenalPay gateway', 'woocommerce'); $this->has_fields = false; // Load settings fields. $this->init_form_fields(); $this->init_settings(); // Get the settings and load them into variables $this->title = $this->get_option('title'); $this->description = $this->get_option('description'); $this->debug = 'yes' === $this->get_option('debug', 'no'); $this->arsenalpay_callback_key = $this->get_option('arsenalpay_callback_key'); $this->arsenalpay_widget_key = $this->get_option('arsenalpay_widget_key'); $this->arsenalpay_widget_id = $this->get_option('arsenalpay_widget_id'); $this->arsenalpay_ip = $this->get_option('arsenalpay_ip'); // Logs if ($this->debug) { $this->loger = new WC_Logger(); } // Add display hook of receipt and save hook for settings: add_action('woocommerce_receipt_arsenalpay', array($this, 'receipt_page')); add_action('woocommerce_update_options_payment_gateways_' . $this->id, array( $this, 'process_admin_options' )); // ArsenalPay callback hook: add_action('woocommerce_api_wc_gw_arsenalpay', array($this, 'callback_listener')); if (!$this->is_valid_for_use()) { $this->enabled = false; } } /** * Check if this gateway is enabled and available in the user's country * * @access public * @return bool */ function is_valid_for_use() { if (!in_array(get_woocommerce_currency(), apply_filters('woocommerce_arsenalpay_supported_currencies', array('RUB')))) { return false; } return true; } private function _get_arsenalpay_taxes() { return array( "none" => "Не облагается", "vat0" => "НДС 0%", "vat10" => "НДС 10%", "vat18" => "НДС 18%", "vat110" => "НДС 10/110", "vat118" => "НДС 18/118" ); } private function _get_all_wc_taxes() { global $wpdb; $query = " SELECT * FROM {$wpdb->prefix}woocommerce_tax_rates WHERE 1 = 1 "; $order_by = ' ORDER BY tax_rate_order'; $result = $wpdb->get_results($query . $order_by); return $result; } public function init_form_fields() { $this->form_fields = array( 'enabled' => array( 'title' => __('Enable/Disable', 'woocommerce'), 'type' => 'checkbox', 'label' => __('Enable ArsenalPay', 'wc-arsenalpay'), 'default' => 'yes' ), 'title' => array( 'title' => __('Title', 'woocommerce'), 'type' => 'text', 'description' => __('This controls the title which the user sees during checkout.', 'woocommerce'), 'default' => __('ArsenalPay', 'woocommerce'), 'desc_tip' => true, ), 'description' => array( 'title' => __('Description', 'woocommerce'), 'type' => 'text', 'default' => __('Pay with ArsenalPay.', 'wc-arsenalpay'), 'description' => __('This controls the description which the user sees during checkout.', 'woocommerce'), 'desc_tip' => true, ), 'debug' => array( 'title' => __('Debug Log', 'woocommerce'), 'type' => 'checkbox', 'label' => __('Enable logging', 'woocommerce'), 'default' => 'no', 'description' => sprintf(__('Log ArsenalPay events, such as callback requests', 'wc-arsenalpay'), wc_get_log_file_path('arsenalpay')) ), 'arsenalpay_widget_id' => array( 'title' => __('Widget ID', 'wc-arsenalpay'), 'type' => 'text', 'description' => __('Assigned to merchant for the access to ArsenalPay payment widget. Required.', 'wc-arsenalpay'), 'desc_tip' => true, ), 'arsenalpay_widget_key' => array( 'title' => __('Widget key', 'wc-arsenalpay'), 'type' => 'text', 'description' => __('Assigned to merchant for the access to ArsenalPay payment widget. Required.', 'wc-arsenalpay'), 'desc_tip' => true, ), 'arsenalpay_callback_key' => array( 'title' => __('Callback key', 'wc-arsenalpay'), 'type' => 'text', 'description' => __('With this key you check a validity of sign that comes with callback payment data. Required.', 'wc-arsenalpay'), 'desc_tip' => true, ), 'arsenalpay_ip' => array( 'title' => __('Allowed IP-address', 'wc-arsenalpay'), 'type' => 'text', 'description' => __('It can be allowed to receive ArsenalPay payment confirmation callback requests only from the ip address pointed out here. Optional.', 'wc-arsenalpay'), 'desc_tip' => true, ), 'default_tax' => array( 'title' => __('Default tax', 'wc-arsenalpay'), 'type' => 'select', 'options' => $this->_get_arsenalpay_taxes(), 'description' => __('The tax rate will default to check if the product is not specified a different rate.', 'wc-arsenalpay'), 'desc_tip' => false, ), ); $WC_taxes = $this->_get_all_wc_taxes(); if (count($WC_taxes) > 0) { $this->form_fields["tax_title"] = array( "title" => __('On the left is the tax rate in your store, right in the Federal Tax Service. Match them.', 'wc-arsenalpay'), "type" => 'title', ); } foreach ($WC_taxes as $wc_tax) { $option_name = $this->_get_option_name_for_tax($wc_tax->tax_rate_id); $this->form_fields[$option_name] = array( "title" => $wc_tax->tax_rate_name . " (" . round($wc_tax->tax_rate) . "%)", "type" => 'select', "options" => $this->_get_arsenalpay_taxes(), ); } } private function _get_option_name_for_tax($tax_id) { return "wc_tax_" . $tax_id; } public function admin_options() { echo "

Redirect URL

"; if ($this->is_valid_for_use()) { ?>

generate_settings_html(); ?>

:

get_user_id(); $total = number_format($order->get_total(), 2, '.', ''); $nonce = md5(microtime(true) . mt_rand(100000, 999999)); $sign_data = "$user_id;$order_id;$total;$this->arsenalpay_widget_id;$nonce"; $widget_sign = hash_hmac('sha256', $sign_data, $this->arsenalpay_widget_key); $html = "
"; echo $html; } public function process_payment($order_id) { $order = wc_get_order($order_id); return array( 'result' => 'success', 'redirect' => $order->get_checkout_payment_url(true) ); } function callback_listener() { global $woocommerce; @ob_clean(); $callback_params = stripslashes_deep($_POST); $REMOTE_ADDR = $_SERVER["REMOTE_ADDR"]; $this->log('Remote IP: ' . $REMOTE_ADDR . ' with POST params: ' . json_encode($callback_params)); $IP_ALLOW = trim($this->arsenalpay_ip); if (strlen($IP_ALLOW) > 0 && $IP_ALLOW != $REMOTE_ADDR) { $this->log('IP ' . $REMOTE_ADDR . ' is not allowed.'); $this->exitf('ERR'); } if (!$this->check_params($callback_params)) { $this->exitf('ERR'); } $this->_callback = $callback_params; $order = wc_get_order($this->_callback['ACCOUNT']); $function = $this->_callback['FUNCTION']; if (!$order || empty($order) || !($order instanceof WC_Order)) { if ($function == "check") { $this->log(" Order %s doesn't exist. ", $this->_callback['ACCOUNT']); $this->exitf('NO'); } $this->exitf("ERR"); } if (!($this->check_sign($this->_callback, $this->arsenalpay_callback_key))) { $this->log('Error in callback parameters ERR_INVALID_SIGN'); $this->exitf('ERR'); } switch ($function) { case 'check': $this->callback_check($order, $woocommerce); break; case 'payment': $this->callback_payment($order, $woocommerce); break; case 'cancel': $this->callback_cancel($order, $woocommerce); break; case 'cancelinit': $this->callback_cancel($order, $woocommerce); break; case 'refund': $this->callback_refund($order, $woocommerce); break; case 'reverse': $this->callback_reverse($order, $woocommerce); break; case 'reversal': $this->callback_reverse($order, $woocommerce); break; case 'hold': $this->callback_hold($order, $woocommerce); break; default: { $order->update_status('failed', sprintf(__('Payment failed via callback.', 'woocommerce'))); $this->log('Error in callback'); $this->exitf('ERR'); } } } private function callback_check($order, $woocommerce) { if ($order->is_paid() || $order->has_status('cancelled') || $order->has_status('refunded')) { $this->log('Aborting, Order #' . $order->get_id() . ' is ' . $order->get_status()); $this->exitf('ERR'); } $isCorrectAmount = ($order->get_total() == $this->_callback['AMOUNT'] && $this->_callback['MERCH_TYPE'] == '0') || ($order->get_total() > $this->_callback['AMOUNT'] && $this->_callback['MERCH_TYPE'] == '1' && $order->get_total() == $this->_callback['AMOUNT_FULL']); if (!$isCorrectAmount) { $this->log('Payment error: Amounts do not match (amount ' . $this->_callback['AMOUNT'] . ')'); // Put this order on-hold for manual checking $order->update_status('on-hold', sprintf(__('Validation error: ArsenalPay amounts do not match (amount %s).', 'woocommerce'), $this->_callback['AMOUNT'])); $this->exitf('ERR'); } $fiscal = array(); if (isset($this->_callback['OFD']) && $this->_callback['OFD'] == 1) { $fiscal = $this->prepareFiscalDocument($order); if (!$fiscal) { $this->log('Error during preparing fiscal document!'); $this->exitf('ERR'); } } $order->update_status('pending', sprintf(__('Order number has been checked.', 'wc-arsenalpay'))); $order->add_order_note(__('Waiting for payment confirmation after checking.', 'wc-arsenalpay')); $this->exitf('YES', $fiscal); } private function preparePhone($phone) { $phone = preg_replace('/[^0-9]/', '', $phone); if (strlen($phone) < 10) { return false; } if (strlen($phone) == 10) { return $phone; } if (strlen($phone) == 11) { return substr($phone, 1); } return false; } private function _get_arsenalpay_tax_rate($taxes) { $default_tax_rate = $this->get_option('default_tax'); $taxes_subtotal = $taxes['total']; if ($taxes_subtotal) { $wc_tax_ids = array_keys($taxes_subtotal); $wc_tax_id = $wc_tax_ids[0]; $arsenalpay_tax_rate = $this->get_option($this->_get_option_name_for_tax($wc_tax_id)); if ($arsenalpay_tax_rate) { return $arsenalpay_tax_rate; } } return $default_tax_rate; } /** * @param $order WC_Order * * @return array */ private function prepareFiscalDocument($order) { $fiscal = array( "id" => $this->_callback['ID'], "type" => "sell", "receipt" => [ "attributes" => [ "email" => $order->get_billing_email(), ], "items" => array(), ] ); $phone = $this->preparePhone($order->get_billing_phone()); if ($phone) { $fiscal['receipt']['attributes']['phone'] = $phone; } /** * @var $line_item WC_Order_Item */ foreach ($order->get_items('line_item') as $line_item) { $item = array( "name" => $line_item->get_name(), "quantity" => $line_item->get_quantity(), "price" => $order->get_item_total($line_item, true, true), "sum" => $order->get_line_total($line_item, true, true), ); $tax = $this->_get_arsenalpay_tax_rate($line_item->get_taxes()); if ($tax) { $item['tax'] = $tax; } $fiscal['receipt']['items'][] = $item; } /** * @var $shipping WC_Order_Item */ foreach ($order->get_items('shipping') as $shipping) { $item = array( "name" => $shipping->get_name(), "quantity" => $shipping->get_quantity(), "price" => $order->get_item_total($shipping, true, true), "sum" => $order->get_line_total($shipping, true, true), ); $tax = $this->_get_arsenalpay_tax_rate($shipping->get_taxes()); if ($tax) { $item['tax'] = $tax; } $fiscal['receipt']['items'][] = $item; } return $fiscal; } private function callback_payment($order, $woocommerce) { if ($order->is_paid() || $order->has_status('cancelled') || $order->has_status('refunded')) { $this->log('Aborting, Order #' . $order->get_id() . ' is ' . $order->get_status()); $this->exitf('ERR'); } if ($order->get_total() == $this->_callback['AMOUNT'] && $this->_callback['MERCH_TYPE'] == '0') { $order->add_order_note(__('Payment completed.', 'wc-arsenalpay')); } elseif ($order->get_total() > $this->_callback['AMOUNT'] && $this->_callback['MERCH_TYPE'] == '1' && $order->get_total() == $this->_callback['AMOUNT_FULL']) { $order->add_order_note(sprintf(__("Payment received with less amount equal to %s.", 'wc-arsenalpay'), $this->_callback['AMOUNT'])); } else { $this->log('Payment error: Amounts do not match (amount ' . $this->_callback['AMOUNT'] . ')'); // Put this order on-hold for manual checking $order->update_status('on-hold', sprintf(__('Validation error: ArsenalPay amounts do not match (amount %s).', 'woocommerce'), $this->_callback['AMOUNT'])); $this->exitf('ERR'); } $order->payment_complete(); $woocommerce->cart->empty_cart(); $this->exitf('OK'); } private function callback_hold($order, $woocommerce) { if (!$order->has_status('on-hold') && !$order->has_status('pending')) { $this->log('Aborting, Order #' . $order->get_id() . ' has not been checked.'); $this->exitf('ERR'); } $isCorrectAmount = ($order->get_total() == $this->_callback['AMOUNT'] && $this->_callback['MERCH_TYPE'] == '0') || ($order->get_total() > $this->_callback['AMOUNT'] && $this->_callback['MERCH_TYPE'] == '1' && $order->get_total() == $this->_callback['AMOUNT_FULL']); if (!$isCorrectAmount) { $this->log('Payment error: Amounts do not match (amount ' . $this->_callback['AMOUNT'] . ')'); // Put this order on-hold for manual checking $order->update_status('on-hold', sprintf(__('Validation error: ArsenalPay amounts do not match (amount %s).', 'woocommerce'), $this->_callback['AMOUNT'])); $this->exitf('ERR'); } $order->update_status('on-hold', sprintf(__('Order number has been holden.', 'wc-arsenalpay'))); $order->add_order_note(__('Waiting for payment or cancel confirmation after hold request.', 'wc-arsenalpay')); $this->exitf('OK'); } private function callback_cancel($order, $woocommerce) { if (!$order->has_status('on-hold') && !$order->has_status('pending')) { $this->log('Aborting, Order #' . $order->get_id() . ' has not been checked status.'); $this->exitf('ERR'); } $order->update_status('cancelled', sprintf(__('Order has been cancelled', 'wc-arsenalpay'))); $this->exitf('OK'); } private function callback_refund($order, $woocommerce) { if (!$order->is_paid() && !$order->has_status('refunded')) { $this->log('Aborting, Order #' . $order->get_id() . ' is not paid.'); $this->exitf('ERR'); } $arsenalPaidSum = $order->get_total() - $order->get_total_refunded(); $isCorrectAmount = ($this->_callback['MERCH_TYPE'] == 0 && $arsenalPaidSum >= $this->_callback['AMOUNT']) || ($this->_callback['MERCH_TYPE'] == 1 && $arsenalPaidSum >= $this->_callback['AMOUNT'] && $arsenalPaidSum >= $this->_callback['AMOUNT_FULL']); if (!$isCorrectAmount) { $this->log("Refund error: Paid amount({$arsenalPaidSum}) < refund amount({$this->_callback['AMOUNT']})"); $this->exitf('ERR'); } $res = wc_create_refund(array( 'amount' => $this->_callback['AMOUNT'], 'reason' => 'Partition refund via Arsenalpay', 'order_id' => $order->get_id(), 'refund_payment' => false, )); if ($res instanceof WP_Error) { $this->log('Error during refund: ' . $res->get_error_message()); $this->exitf('ERR'); } $this->exitf('OK'); } private function callback_reverse($order, $woocommerce) { if (!$order->is_paid()) { $this->log('Aborting, Order #' . $order->get_id() . ' is not complete.'); $this->exitf('ERR'); } $arsenalPaidSum = $order->get_total() - $order->get_total_refunded(); $isCorrectAmount = ($this->_callback['MERCH_TYPE'] == 0 && $arsenalPaidSum == $this->_callback['AMOUNT']) || ($this->_callback['MERCH_TYPE'] == 1 && $arsenalPaidSum >= $this->_callback['AMOUNT'] && $arsenalPaidSum == $this->_callback['AMOUNT_FULL']); if (!$isCorrectAmount) { $this->log('Reverse error: Amounts do not match (amount ' . $this->_callback['AMOUNT'] . ' and ' . $arsenalPaidSum . ')'); $this->exitf('ERR'); } $order->update_status('refunded', sprintf(__('Order has been reversed', 'wc-arsenalpay'))); $this->exitf('OK'); } private function check_sign($callback_params, $pass) { $validSign = ($callback_params['SIGN'] === md5(md5($callback_params['ID']) . md5($callback_params['FUNCTION']) . md5($callback_params['RRN']) . md5($callback_params['PAYER']) . md5($callback_params['AMOUNT']) . md5($callback_params['ACCOUNT']) . md5($callback_params['STATUS']) . md5($pass))) ? true : false; return $validSign; } private function check_params($callback_params) { $required_keys = array ( 'ID', /* Merchant identifier */ 'FUNCTION', /* Type of request to which the response is received*/ 'RRN', /* Transaction identifier */ 'PAYER', /* Payer(customer) identifier */ 'AMOUNT', /* Payment amount */ 'ACCOUNT', /* Order number */ 'STATUS', /* When /check/ - response for the order number checking, when // payment/ - response for status change.*/ 'DATETIME', /* Date and time in ISO-8601 format, urlencoded.*/ 'SIGN', /* Response sign = md5(md5(ID).md(FUNCTION).md5(RRN).md5(PAYER).md5(AMOUNT). // md5(ACCOUNT).md(STATUS).md5(PASSWORD)) */ ); /** * Checking the absence of each parameter in the post request. */ foreach ($required_keys as $key) { if (empty($callback_params[$key]) || !array_key_exists($key, $callback_params)) { $this->log('Error in callback parameters ERR' . $key); return false; } else { $this->log(" $key=$callback_params[$key]"); } } if ($callback_params['FUNCTION'] != $callback_params['STATUS']) { $this->log("Error: FUNCTION ({$callback_params['FUNCTION']} not equal STATUS ({$callback_params['STATUS']})"); return false; } return true; } /** * Log file path: /wp-content/uploads/wc-logs/log-[a-z0-9]+.log * * @param $msg */ private function log($msg) { if ($this->debug) { $this->loger->log('debug', "Arsenalpay: " . $msg); } } public function exitf($msg, $ofd = array()) { if (isset($this->_callback['FORMAT']) && $this->_callback['FORMAT'] == 'json') { $response = array("response" => $msg); if (isset($this->_callback['OFD']) && $this->_callback['OFD'] == 1 && $ofd) { $response['ofd'] = $ofd; } $msg = json_encode($response); } $this->log(" $msg "); echo $msg; die; } } /** * Add the ArsenalPay Gateway to WooCommerce **/ function add_wc_arsenalpay($methods) { $methods[] = 'WC_GW_ArsenalPay'; return $methods; } add_filter('woocommerce_payment_gateways', 'add_wc_arsenalpay'); }