vindi/vindi-woocommerce

View on GitHub
src/utils/PaymentGateway.php

Summary

Maintainability
A
3 hrs
Test Coverage
<?php

namespace VindiPaymentGateways;

use WC_Payment_Gateway_CC;
use Exception;
use WC_Order;
use WP_Error;

if (!defined('ABSPATH')) {
  exit;
}

include_once VINDI_PATH . 'src/services/VindiHelpers.php';

/**
 * Abstract class that will be inherited by all payment methods.
 *
 * @extends WC_Payment_Gateway_CC
 *
 * @since 1.0.0
 */

abstract class VindiPaymentGateway extends WC_Payment_Gateway_CC
{
  /**
   * @var bool
   */
  protected $validated = true;

  /**
   * @var VindiSettings
   */
  public $vindi_settings;

  /**
   * @var VindiControllers
   */
  public $controllers;

  /**
   * @var VindiLogger
   */
  protected $logger;

  /**
   * @var VindiRoutes
   */
  protected $routes;

  /**
   * Should return payment type for payment processing.
   * @return string
   */
  public abstract function type();

  public function __construct(VindiSettings $vindi_settings, VindiControllers $controllers)
  {
    $this->vindi_settings = $vindi_settings;
    $this->controllers = $controllers;
    $this->logger = $this->vindi_settings->logger;
    $this->routes = $vindi_settings->routes;
    $this->title = $this->get_option('title');
    $this->enabled = $this->get_option('enabled');

    if (is_admin()) {
      add_action('woocommerce_update_options_payment_gateways_' . $this->id, array(&$this, 'process_admin_options'));
    }
  }

  /**
   * Create the level 3 data array to send to Vindi when making a purchase.
   *
   * @param WC_Order $order The order that is being paid for.
   * @return array          The level 3 data to send to Vindi.
   */
  public function get_level3_data_from_order($order)
  {
    // WC Versions before 3.0 don't support postcodes and are
    // incompatible with level3 data.
    if (VindiHelpers::is_wc_lt('3.0')) {
      return array();
    }

    // Get the order items. Don't need their keys, only their values.
    // Order item IDs are used as keys in the original order items array.
    $order_items = array_values($order->get_items());
    $currency    = $order->get_currency();

    $vindi_line_items = array_map(function ($item) use ($currency) {
      $product_id          = $item->get_variation_id()
        ? $item->get_variation_id()
        : $item->get_product_id();
      $product_description = substr($item->get_name(), 0, 26);
      $quantity            = $item->get_quantity();
      $unit_cost           = VindiHelpers::get_vindi_amount(($item->get_subtotal() / $quantity), $currency);
      $tax_amount          = VindiHelpers::get_vindi_amount($item->get_total_tax(), $currency);
      $discount_amount     = VindiHelpers::get_vindi_amount($item->get_subtotal() - $item->get_total(), $currency);

      return (object) array(
        'product_code'        => (string) $product_id, // Up to 12 characters that uniquely identify the product.
        'product_description' => $product_description, // Up to 26 characters long describing the product.
        'unit_cost'           => $unit_cost, // Cost of the product, in cents, as a non-negative integer.
        'quantity'            => $quantity, // The number of items of this type sold, as a non-negative integer.
        'tax_amount'          => $tax_amount, // The amount of tax this item had added to it, in cents, as a non-negative integer.
        'discount_amount'     => $discount_amount, // The amount an item was discounted—if there was a sale,for example, as a non-negative integer.
      );
    }, $order_items);

    $level3_data = array(
      'merchant_reference'   => $order->get_id(), // An alphanumeric string of up to  characters in length. This unique value is assigned by the merchant to identify the order. Also known as an “Order ID”.

      'shipping_amount'      => VindiHelpers::get_vindi_amount($order->get_shipping_total() + $order->get_shipping_tax(), $currency), // The shipping cost, in cents, as a non-negative integer.
      'line_items'           => $vindi_line_items,
    );

    $shipping_address_zip = $order->get_shipping_postcode();

    if ($this->is_valid_br_zip_code($shipping_address_zip)) {

      $level3_data['shipping_address_zip'] = $shipping_address_zip;
    }

    $store_postcode = get_option('woocommerce_store_postcode');
    if ($this->is_valid_br_zip_code($store_postcode)) {
      $level3_data['shipping_from_zip'] = $store_postcode;
    }

    return $level3_data;
  }

  /**
   * Verifies whether a certain ZIP code is valid for the BRL, incl. 8-digit extensions.
   *
   * @param string $zip The ZIP code to verify.
   * @return boolean
   */
  public function is_valid_br_zip_code($zip)
  {
    return !empty($zip) && preg_match('/^[0-9]{5,5}([- ]?[0-9]{3,3})?$/', $zip);
  }

  /**
   * Admin Panel Options
   */
  public function admin_options()
  {
      $this->vindi_settings->get_template('admin-gateway-settings.html.php', array('gateway' => $this));
  }

  /**
   * Get the users country either from their order, or from their customer data
   * @return string|null
   */
  public function get_country_code()
  {
    if (isset($_GET['order_id'])) {
      $order = new WC_Order(filter_var($_GET['order_id'], FILTER_SANITIZE_NUMBER_INT));
      return $order->billing_country;
    } elseif ($this->vindi_settings->woocommerce->customer->get_billing_country()) {
      return $this->vindi_settings->woocommerce->customer->get_billing_country();
    }
  }

  /**
   * Validate plugin settings
   * @return bool
   */
  public function validate_settings()
  {
    $currency = get_option('woocommerce_currency');
    $api_key = $this->vindi_settings->get_api_key();
    return in_array($currency, ['BRL']) && ! empty($api_key);
  }

  /**
   * Process the payment
   *
   * @param int $order_id
   *
   * @return array
   */
  public function process_payment($order_id)
  {
    $order_id = filter_var($order_id, FILTER_SANITIZE_NUMBER_INT);
    $this->logger->log(sprintf('Processando pedido %s.', $order_id));
    $order   = wc_get_order($order_id);
    $payment = new VindiPaymentProcessor($order, $this, $this->vindi_settings, $this->controllers);

    // exit if validation by validate_fields() fails
    if (! $this->validated) {
      return false;
    }

    // Validate plugin settings
    if (! $this->validate_settings()) {
      return $payment->abort(__('O Pagamento foi cancelado devido a erro de configuração do meio de pagamento.', VINDI));
    }

    try {
      $response = $payment->process();
      $order->reduce_order_stock();
    } catch (Exception $e) {
      $response = array(
        'result'   => 'fail',
        'redirect' => '',
      );
    }

    return $response;
  }

  /**
   * Check if the order is a Single Payment Order (not a Subscription).
   * @return bool
   */
  protected function is_single_order()
  {
    $types = [];

    foreach ($this->vindi_settings->woocommerce->cart->cart_contents as $item) {
      $types[] = $item['data']->get_type();
    }

    return !(boolean) preg_grep('/subscription/', $types);
  }

  /**
   * Process a refund.
   *
   * @param  int    $order_id Order ID.
   * @param  float  $amount Refund amount.
   * @param  string $reason Refund reason.
   * @return bool|WP_Error
   */
  public function process_refund($order_id, $amount = null, $reason = '') {
    $order_id = filter_var($order_id, FILTER_SANITIZE_NUMBER_INT);
    $amount = filter_var($amount, FILTER_SANITIZE_NUMBER_FLOAT);
    $reason = sanitize_text_field($reason);
    $order = wc_get_order($order_id);

    if (!$this->can_refund_order($order)) {
      return new WP_Error('error', __('Reembolso falhou.', VINDI));
    }

    if($amount < $order->get_total()) {
      return new WP_Error('error', __('Não é possível realizar reembolsos parciais, faça um reembolso manual caso você opte por esta opção.', VINDI));
    }

    $results = [];
        $order_meta = $order->get_meta('vindi_order', true);
    foreach ($order_meta as $key => $order_item) {
      $bill_id = $order_item['bill']['id'];

      $result = $this->refund_transaction($bill_id, $amount, $reason);

      $this->logger->log('Resultado do reembolso: ' . wc_print_r($result, true));
      switch (strtolower($result['status'])) {
        case 'success':
          $order->add_order_note(
            /* translators: 1: Refund amount, 2: Refund ID */
            sprintf(__('[Transação #%2$s]: reembolsado R$%1$s', VINDI), $result['amount'], $result['id'])
          );
          break;
      }
      if(isset($result->errors)) {
        throw new Exception($result->errors[0]->message);
        return false;
      }
    }
    return true;
  }

  /**
     * Get refund request args.
     *
     * @param  int      $bill_id Order object.
     * @param  float    $amount Refund amount.
     * @param  string   $reason Refund reason.
     * @return array
     */
    public function get_refund_request($bill_id, $amount = null, $reason = '') {
    $bill_id = filter_var($bill_id, FILTER_SANITIZE_NUMBER_INT);
    $amount = filter_var($amount, FILTER_SANITIZE_NUMBER_FLOAT);
    $reason = sanitize_text_field($reason);
        $request = array(
            'cancel_bill' => true,
            'comments' => strip_tags(wc_trim_string($reason, 255)),
            'amount' => null
        );
        if (!is_null($amount) ) {
            $request['amount'] = substr($amount, 0, strlen($amount)-2) . "." . substr($amount, -2);
        }
        return apply_filters('vindi_refund_request', $request, $bill_id, $request['amount'], $reason);
  }

  /**
     * Refund an order via PayPal.
     *
     * @param  int      $bill_id Order object.
     * @param  float    $amount Refund amount.
     * @param  string   $reason Refund reason.
     * @return object Either an object of name value pairs for a success, or a WP_ERROR object.
     */
    public function refund_transaction($bill_id, $amount = null, $reason = '') {
    $bill_id = filter_var($bill_id, FILTER_SANITIZE_NUMBER_INT);
    $amount = filter_var($amount, FILTER_SANITIZE_NUMBER_FLOAT);
    $reason = sanitize_text_field($reason);

    $data = $this->get_refund_request($bill_id, $amount, $reason);
    $last_charge = $this->find_bill_last_charge($bill_id);
    $charge_id = $last_charge['id'];
    $refund = $this->routes->refundCharge($charge_id, $data);

        if (empty($refund)) {
            return new WP_Error('error', __('Reembolso na Vindi falhou. Verifique o log do plugin', VINDI));
        }

        return $refund['last_transaction'];
  }

  private function find_bill_last_charge($bill_id)
  {
    $bill_id = filter_var($bill_id, FILTER_SANITIZE_NUMBER_INT);
    $bill = $this->routes->findBillById($bill_id);

    if(!$bill) {
      throw new Exception(sprintf(__('A fatura com bill_id #%s não foi encontrada!', VINDI), $bill_id), 2);
    }

    $charges = $bill['charges'];
    return end($charges);
    }
};