vindi/vindi-woocommerce

View on GitHub
src/services/Webhooks.php

Summary

Maintainability
C
1 day
Test Coverage
<?php

namespace VindiPaymentGateways;

use WC_Subscriptions_Manager;
use Exception;
use WP_Query;
use WC_Order;

class VindiWebhooks
{
  /**
   * @var VindiSettings
   */
  private $vindi_settings;

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

  /**
   * @var WebhooksHelper
   */
    private $webhooksHelpers;

  /**
   * @param VindiSettings $vindi_settings
   */
    public function __construct(VindiSettings $vindi_settings)
    {
      $this->vindi_settings = $vindi_settings;
      $this->routes = $vindi_settings->routes;
        $this->webhooksHelpers = new WebhooksHelpers($this);
    }

  /**
   * Handle incoming webhook.
   */
    public function handle()
    {
        $token = filter_input(INPUT_GET, 'token', FILTER_SANITIZE_STRING);
        $raw_body = file_get_contents('php://input');
        $body = json_decode($raw_body);

        if (!$this->validate_access_token($token)) {
            http_response_code(403);
            die('invalid access token');
        }

        $this->vindi_settings->logger->log(sprintf(__('Novo Webhook chamado: %s', VINDI), $raw_body));

        try {
            $this->process_event($body);
        } catch (Exception $e) {
            $this->vindi_settings->logger->log($e->getMessage());

            if (2 === $e->getCode()) {
                header("HTTP/1.0 422 Unprocessable Entity");
                die($e->getMessage());
            }
        }
    }

  /**
   * @param string $token
   */
  private function validate_access_token($token)
  {
    return $token === $this->vindi_settings->get_token();
  }

  /**
   * Read json entity received and proccess the right event
   * @param string $body
   */
  private function process_event($body)
  {
    if (null == $body || empty($body->event))
      throw new Exception(__('Falha ao interpretar JSON do webhook: Evento do Webhook não encontrado!', VINDI));

    $type = $body->event->type;
    $data = $body->event->data;

    if (method_exists($this, $type)) {
      $this->vindi_settings->logger->log(sprintf(__('Novo Evento processado: %s', VINDI), $type));
      return $this->{$type}($data);
    }

    $this->vindi_settings->logger->log(sprintf(__('Evento do webhook ignorado pelo plugin: ', VINDI), $type));
  }

  /**
   * Process test event from webhook
   * @param $data array
   */
  private function test($data)
  {
    $this->vindi_settings->logger->log(__('Evento de teste do webhook.', VINDI));
  }

    /**
   * Process bill_created event from webhook
   * @param $data array
   */
    private function bill_created($data)
    {
        $response = ['message' => 'Não foi possível emitir a fatura', 'status' => 422];
        try {
            if (empty($data->bill->subscription)) {
                return;
            }

            $renewInfos = [
              'wc_subscription_id' => $data->bill->subscription->code,
              'vindi_subscription_id' => $data->bill->subscription->id,
              'plan_name' => str_replace('[WC] ', '', $data->bill->subscription->plan->name),
              'cycle' => $data->bill->period->cycle,
              'bill_status' => $data->bill->status,
              'bill_id' => $data->bill->id,
              'bill_print_url' => $data->bill->charges[0]->print_url
            ];

            if ($this->webhooksHelpers->handle_subscription_renewal($renewInfos, $data)) {
                $response = ['message' => 'Fatura emitida corretamente', 'status' => 200];
            } elseif ($this->webhooksHelpers->handle_trial_period($renewInfos['wc_subscription_id'])) {
                $response = ['message' => 'O estado da assinatura passou para "Em espera"', 'status' => 200];
            }
        } catch (\Exception $e) {
            $this->handle_exception('bill_created', $e->getMessage(), $data->bill->id);
            $response = ['message' => 'Erro durante o processamento da fatura.', 'status' => 500];
        }

        return wp_send_json(['message' => $response['message']], $response['status']);
    }
  
  /**
   * Process subscription_renew event from webhook
   * @param $renew_infos array
   */
    public function subscription_renew($renew_infos)
  {
    $subscription = $this->find_subscription_by_id($renew_infos['wc_subscription_id']);

    if ($this->subscription_has_order_in_cycle($renew_infos['vindi_subscription_id'], $renew_infos['cycle'])) {
      throw new Exception(sprintf(
        __('Já existe o ciclo %s para a assinatura #%s pedido #%s!', VINDI),
        $renew_infos['cicle'],
        $renew_infos['vindi_subscription_id'],
        $subscription->get_last_order()
      ));
    }

    WC_Subscriptions_Manager::prepare_renewal($subscription->id);
    $order_id = $subscription->get_last_order();
    $order = $this->find_order_by_id($order_id);
    $subscription_id = $renew_infos['vindi_subscription_id'];
        $order_post_meta = array($order->get_meta('vindi_order', true));
    $order_post_meta[$subscription_id]['cycle'] = $renew_infos['cycle'];
    $order_post_meta[$subscription_id]['product'] = $renew_infos['plan_name'];
    $order_post_meta[$subscription_id]['bill'] = array(
      'id' => $renew_infos['bill_id'],
      'status' => $renew_infos['bill_status'],
      'bank_slip_url' => $renew_infos['bill_print_url'],
    );
        $order->update_meta_data('vindi_order', $order_post_meta);
        $order->save();
    $this->vindi_settings->logger->log('Novo Período criado: Pedido #'.$order->id);

    // We've already processed the renewal
    remove_action('woocommerce_scheduled_subscription_payment', 'WC_Subscriptions_Manager::prepare_renewal');
    }

  /**
   * Process bill_paid event from webhook
   * @param $data array
   */
    private function bill_paid($data)
    {
        try {
            if (empty($data->bill->subscription)) {
              $order = $this->find_order_by_id($data->bill->code);
            } else {
                $vindi_subscription_id = $data->bill->subscription->id;
                $cycle = $data->bill->period->cycle;
                $order = $this->find_order_by_subscription_and_cycle($vindi_subscription_id, $cycle);
            }

            $vindi_order = get_post_meta($order->id, 'vindi_order', true);
            if (!is_array($vindi_order)) {
                return wp_send_json(['message' => 'Pedido Vindi não encontrado.'], 422);
            }

            if (empty($data->bill->subscription)) {
                $vindi_order['single_payment']['bill']['status'] = $data->bill->status;
            }
            $vindi_order[$vindi_subscription_id]['bill']['status'] = $data->bill->status;
            $order->update_meta_data('vindi_order', $vindi_order);
            $order->save();
            $vindi_order_info = end($vindi_order);

            if ($vindi_order_info['bill']['status'] == 'paid') {
                $new_status = $this->vindi_settings->get_return_status();
                $order->update_status($new_status, __('O pagamento foi processado com sucesso pela Vindi.', VINDI));
                $this->update_next_payment($data);
                return wp_send_json(['message' => 'Processamento de pagamento de fatura concluído.'], 200);
            }
            return wp_send_json(['message' => 'Não foi possível processar o pagamento da fatura'], 422);
        } catch (\Exception $e) {
            $this->handle_exception('bill_paid', $e->getMessage(), $data->bill->code);
            return wp_send_json(['message' => 'Erro durante o processamento do pagamento da fatura.'], 500);
        }
    }

  /**
   * Process bill_canceled event from webhook
   * @param $data array
   */
  private function bill_canceled($data)
  {
    try {
      if (empty($data->bill->subscription)) {
        $order = $this->find_order_by_id($data->bill->code);
      } else {
        $vindi_subscription_id = $data->bill->subscription->id;
        $cycle = $data->bill->period->cycle;
        $order = $this->find_order_by_subscription_and_cycle($vindi_subscription_id, $cycle);
      }

      $order->update_status('cancelled', __('Pagamento cancelado dentro da Vindi!', VINDI));
            return wp_send_json(['message' => 'Pagamento cancelado dentro da Vindi!'], 200);
    } catch (Exception $e) {
            $this->handle_exception('bill_canceled', $e->getMessage(), $data->bill->code);
            return wp_send_json(['message' => 'Erro durante o processamento de cancelamento da fatura.'], 500);
    }
  }

  /**
   * Process issue_created event from webhook
   * @param $data array
   */
  private function issue_created($data)
  {
    $issue_type = $data->issue->issue_type;
    $issue_status = $data->issue->status;
    $item_type = strtolower($data->issue->item_type);

    if ('charge_underpay' !== $issue_type)
      throw new Exception(sprintf(__('Pendência criada com o tipo "%s" não processada!', VINDI), $issue_type));

    if ('open' !== $issue_status)
      throw new Exception(sprintf(__('Pendência criada com o status "%s" não processada!', VINDI), $issue_status));

    if ('charge' !== $item_type)
      throw new Exception(sprintf(__('Pendência criada com o item do tipo "%s" não processada!', VINDI), $item_type));

    $item_id = (int) $data->issue->item_id;
    $issue_data = $data->issue->data;
    $bill = $this->find_bill_by_charge_id($item_id);
    $order = $this->find_order_by_bill_id($bill->id);

    $order->add_order_note(sprintf(
      __('Divergencia de valores do Pedido #%s: Valor Esperado R$ %s, Valor Pago R$ %s.', VINDI),
      $order->id,
      $issue_data->expected_amount,
      $issue_data->transaction_amount
    ));
  }

  /**
   * Process charge_rejected event from webhook
   * @param $data array
   */
    private function charge_rejected($data)
    {
      try {
            $order = $this->find_order_by_bill_id($data->charge->bill->id);
            if (!empty($order) && $order->get_status() == 'pending') {
                $order->update_status('failed', __('Pagamento rejeitado!', VINDI));
                return wp_send_json(['mensagem' => 'O pagamento foi rejeitado com sucesso.'], 200);
            }
            return wp_send_json(['mensagem' => 'Erro ao trocar status da fatura para "failed"
              pois a fatura #%s não está mais pendente!', $data->charge->bill->id], 404);
      } catch (Exception $e) {
            $this->handle_exception_rejected($e, $data);
            return wp_send_json(['mensagem' => 'Ocorreu erro na alteração da assinatura'], 500);
      }
    }

  /**
   * Process handle_exception_rejected
   * @param $e array, $data array
   */
    private function handle_exception_rejected($event, $data)
    {
        if ($event->getCode() == 2) {
            $bill = $this->routes->findBillById($data->charge->bill->id);
            $vindi_subscription_id = isset($bill['subscription']) ? $bill['subscription']['id'] : null;
            $cycle = isset($bill['period']) ? $bill['period']['cycle'] : null;
            $this->find_order_by_subscription_and_cycle($vindi_subscription_id, $cycle);
        }
        $this->handle_exception('charge_rejected', $event->getMessage(), $data->charge->bill->id);
    }

  /**
   * Process subscription_canceled event from webhook
   * @param $data array
   */
    private function subscription_canceled($data)
    {
        try {
            $subscription = $this->find_subscription_by_id($data->subscription->code);
            if ($this->subscription_cancellation_not_possible($subscription, $data)) {
                return wp_send_json(['mensagem' => 'Não foi possível cancelar a assinatura
                      devido ao seu status atual.'], 422);
            }
            if ($this->vindi_settings->dependencies->is_wc_memberships_active()) {
                return $this->handle_pending_cancel($subscription);
            }
            $synchronized_subscription = $this->routes->getSubscription($data->subscription->id);
            if ($synchronized_subscription['status'] === 'canceled') {
                $subscription->update_status('cancelled');
                $this->vindi_settings->logger->log(sprintf(__('Assinatura cancelado.', VINDI)));
                return wp_send_json(['mensagem' => 'Assinatura cancelado.'], 200);
            }
            $this->vindi_settings->logger->log(sprintf(__('Ocorreu um erro no cancelamento da assinatura', VINDI)));
            wp_send_json(['mensagem' => 'Ocorreu erro na assinatura'], 422);
        } catch (\Exception $e) {
            $this->handle_exception('subscription_canceled', $e->getMessage(), $data->subscription->id);
            $response = ['mensagem' => 'Ocorreu erro no cancelamento da assinatura'];
            wp_send_json($response, 500);
            return;
        }
    }

    private function subscription_cancellation_not_possible($subscription, $data)
    {
        return $this->vindi_settings->get_synchronism_status()
        && (
        $subscription->has_status('cancelled')
        || $subscription->has_status('pending-cancel')
        || $subscription->has_status('on-hold')
        )
          || $this->routes->hasPendingSubscriptionBills($data->subscription->id);
    }

    private function handle_pending_cancel($subscription)
    {
        $subscription->update_status('pending-cancel');
        $this->vindi_settings->logger->log(sprintf(__('Assinatura atualizada para cancelamento pendente.', VINDI)));
        return wp_send_json(['message' => 'Assinatura atualizada para cancelamento pendente.'], 200);
    }

  /**
   * Process subscription_reactivated event from webhook
   * @param $data array
   */
    private function subscription_reactivated($data)
    {
        try {
            if ($this->vindi_settings->get_synchronism_status()
              && !$this->routes->hasPendingSubscriptionBills($data->subscription->id)
            ) {
                $subscription_id = $data->subscription->code;
                $subscription = $this->find_subscription_by_id($subscription_id);
                $order_id = $subscription->get_last_order();
                $order = $this->find_order_by_id($order_id);
                $status_available = array('processing', 'completed', 'on-hold');
                if (in_array($order->get_status(), $status_available)) {
                    $subscription->update_status(
                        'active',
                        sprintf(__('Subscription %s reactivated by Vindi.', VINDI), $subscription_id)
                    );
                    return wp_send_json(['message' => 'A assinatura foi reativada com sucesso.'], 200);
                }
            }
            return wp_send_json(['message' => 'A assinatura não pôde ser reativada.'], 422);
        } catch (\Exception $e) {
            $this->handle_exception('subscription_reactivated', $e->getMessage(), $data->subscription->id);
            return wp_send_json(['message' => 'Erro durante o processamento da reativação da assinatura.'], 500);
        }
    }

    private function handle_exception($event, $error, $data)
    {
        $message = print_r([
        'event' => $event,
        'mensagem' => $error,
        "id" => $data,
        ], true);
        $this->vindi_settings->logger->log(sprintf(__('WEBHOOK ERROR: %s', VINDI), $message));
    }

  /**
   * find a subscription by id
   * @param int id
   * @return WC_Subscription
   */
    public function find_subscription_by_id($id_item)
  {
    // Webhooks Ids has "WC-" prefix
        $sanitized_id = explode('WC-', $id_item);
    $subscription = wcs_get_subscription(end($sanitized_id));

    if (empty($subscription))
        throw new Exception(sprintf(__('Assinatura #%s não encontrada!', VINDI), $id_item), 2);

    return $subscription;
    }

  /**
   * @param int id
   *
   * @return WC_Subscription
   */
  private function find_bill_by_charge_id($id)
  {
    $charge = $this->routes->getCharge($id);

    if (empty($charge))
      throw new Exception(sprintf(__('Cobrança #%s não encontrada!', VINDI), $id), 2);

    return (object) $charge['bill'];
  }

  /**
   * find a order by id
   * @param int id
   *
   * @return WC_Order
   */
  private function find_order_by_id($id)
  {
    $order = wc_get_order($id);

    if (empty($order))
      throw new Exception(sprintf(__('Pedido #%s não encontrado!', VINDI), $id), 2);

    return $order;
  }

  /**
   * find orders by bill_id meta
   *
   * @param int $bill_id
   *
   * @return WC_Order
   */
    private function find_order_by_bill_id($bill_id)
  {
    $args = array(
      'post_type' => 'shop_order',
      'meta_key' => 'vindi_bill_id',
      'meta_value' => $bill_id,
      'post_status' => 'any',
    );

    $query = new WP_Query($args);

    if (false === $query->have_posts())
      throw new Exception(sprintf(__('Pedido com bill_id #%s não encontrado!', VINDI), $bill_id), 2);

    return wc_get_order($query->post->ID);
    }

  /**
   * Query orders containing cycle meta
   *
   * @param int $subscription_id
   * @param int $cycle
   *
   * @return WC_Order
   */
    private function find_order_by_subscription_and_cycle($subscription_id, $cycle)
    {
    $query = $this->query_order_by_metas(array(
      array(
        'key' => 'vindi_order',
        'value' => 'i:' . $subscription_id . ';a:3:{s:5:"cycle";i:' . $cycle . ';',
        'compare' => 'LIKE'
      ),
    ));

    if (false === $query->have_posts())
      throw new Exception(sprintf(__('Pedido da assinatura #%s para o ciclo #%s não encontrado!', VINDI), $subscriptionn_id, $cycle), 2);

    return wc_get_order($query->post->ID);
    }

  /**
   * @param int $subscription_id
   * @param int $cycle
   *
   * @return boolean
   */
    public function subscription_has_order_in_cycle($subscription_id, $cycle)
    {
    $query = $this->query_order_by_metas(array(
      array(
        'key' => 'vindi_order',
        'value' => 'i:' . $subscription_id . ';a:3:{s:5:"cycle";i:' . $cycle . ';',
        'compare' => 'LIKE'
      ),
    ));

    return $query->have_posts();
    }

  /**
   * @param array $metas
   *
   * @return WP_Query
   */
    private function query_order_by_metas(array $metas)
    {
    $args = array(
      'post_type' => 'shop_order',
      'meta_query' => $metas,
      'post_status' => 'any',
    );

    return new WP_Query($args);
    }

  /**
   * Update next payment schedule of subscription
   *
   * @param $data object
   */
    public function update_next_payment($data)
  {
    // let's find the subscription in the API
    // we need this step because the actual next billing date does not come from the /bill webhook
    $vindi_subscription = $this->routes->getSubscription($data->bill->subscription->id);

    if ($vindi_subscription && isset($vindi_subscription['next_billing_at'])) {

      $next_billing_at = $vindi_subscription['next_billing_at'];

      $end_at = $vindi_subscription['end_at'];

      // na api, quando o plano é de cobrança única,
      // o next_billing_at é 1 segundo maior que o end_at
      // quando isso acontecer, o next_payment do wc deve ser null
      // (a issue #134 tem mais informações do problema)

      if ($end_at != null && $next_billing_at > $end_at) {
        return false;
      }

      // format next payment date
      $next_payment = $this->format_date($next_billing_at);

      // format end date
      $end_date = $this->format_date($end_at);

      // find our wc_subscription
      $subscription = $this->find_subscription_by_id($data->bill->subscription->code);

      // update the subscription dates
      $subscription->update_dates(array('next_payment' => $next_payment));
      $subscription->update_dates(array('end_date' => $end_date));
    }
    }

  private function format_date($date)
  {
    return date('Y-m-d H:i:s', strtotime($date));
  }
}