src/utils/PaymentProcessor.php
<?php
namespace VindiPaymentGateways;
use DateTime;
use WC_Order;
use Exception;
use WC_Product;
use WC_Order_Item_Product;
use WC_Subscriptions_Coupon;
use WC_Subscriptions_Product;
/**
* @SuppressWarnings(PHPMD.CouplingBetweenObjects)
*/
class VindiPaymentProcessor
{
/**
* Order type is invalid.
*/
const ORDER_TYPE_INVALID = 0;
/**
* Order type is Subscription Payment.
*/
const ORDER_TYPE_SUBSCRIPTION = 1;
/**
* Order type is Single Payment.
*/
const ORDER_TYPE_SINGLE = 2;
/**
* @var WC_Order
*/
private $order;
/**
* @var VindiPaymentGateway
*/
private $gateway;
/**
* @var VindiSettings
*/
private $vindi_settings;
/**
* @var VindiLogger
*/
private $logger;
/**
* @var VindiRoutes
*/
private $routes;
/**
* @var VindiControllers
*/
private $controllers;
private $single_freight;
/**
* Payment Processor contructor.
*
* @param WC_Order $order The order to be processed
* @param VindiPaymentGateway $gateway Current payment gateway
* @param VindiSettings $vindi_settings The VindiSettings instance
* @param VindiControllers $controllers VindiControllers instance
*/
public function __construct(WC_Order $order, VindiPaymentGateway $gateway, VindiSettings $vindi_settings, VindiControllers $controllers)
{
$this->order = $order;
$this->gateway = $gateway;
$this->vindi_settings = $vindi_settings;
$this->logger = $vindi_settings->logger;
$this->routes = $vindi_settings->routes;
$this->controllers = $controllers;
$this->single_freight = $this->vindi_settings->get_option('shipping_and_tax_config') == "yes" ? true : false;
}
/**
* Check if the order contains any subscription.
*
* @return int Order type
*/
public function get_order_type()
{
if (function_exists('wcs_order_contains_subscription') && wcs_order_contains_subscription($this->order, array('any'))) {
return static::ORDER_TYPE_SUBSCRIPTION;
}
return static::ORDER_TYPE_SINGLE;
}
/**
* Find or create a customer within Vindi using the given credentials.
*
* @return array Vindi customer array
*/
public function get_customer()
{
$current_user = $this->order->get_user();
$vindi_customer = [];
if ($current_user->ID) {
$vindi_customer = $this->controllers->customers->update($current_user->ID, $this->order);
}
if (isset($vindi_customer['id']) && $this->is_cc()) {
$this->create_payment_profile($vindi_customer['id']);
}
return $vindi_customer;
}
/**
* Create payment profile for customer using bank slip
*
* @param $customer_id Vindi customer id
*
* @throws Exception
*
*/
public function create_payment_profile_bank_slip($customer_id)
{
if ($this->is_bank_slip()) {
$payment_info = $this->get_bank_slip_payment_type($customer_id);
$payment_profile = $this->routes->createCustomerPaymentProfile($payment_info);
if (!$payment_profile) {
$this->abort(__('Falha ao registrar o método de pagamento. Verifique os dados e tente novamente.', VINDI), true);
}
if ($this->gateway->verify_method()) {
$this->verify_payment_profile($payment_profile['id']);
}
}
}
/**
* Build the credit card payment type.
*
* @param int $customer_id Vindi customer id
*
* @return array
*/
public function get_cc_payment_type($customer_id)
{
if ($this->gateway->verify_user_payment_profile()) {
return false;
}
return array(
'customer_id' => $customer_id,
'holder_name' => sanitize_text_field($_POST['vindi_cc_fullname']),
'card_expiration' => filter_var($_POST['vindi_cc_monthexpiry'], FILTER_SANITIZE_NUMBER_INT) . '/' . filter_var($_POST['vindi_cc_yearexpiry'], FILTER_SANITIZE_NUMBER_INT),
'card_number' => filter_var($_POST['vindi_cc_number'], FILTER_SANITIZE_NUMBER_INT),
'card_cvv' => filter_var($_POST['vindi_cc_cvc'], FILTER_SANITIZE_NUMBER_INT),
'payment_method_code' => $this->payment_method_code(),
'payment_company_code' => sanitize_text_field($_POST['vindi_cc_paymentcompany']),
);
}
/**
* Build the bank slip payment type.
*
* @param int $customer_id Vindi customer id
*
* @return array
*/
public function get_bank_slip_payment_type($customer_id)
{
return array(
'customer_id' => $customer_id,
'payment_method_code' => $this->payment_method_code(),
);
}
/**
* Check if payment method is "Credit Card"
*
* @return bool
*/
public function is_cc()
{
return 'cc' === $this->gateway->type();
}
/**
* Check if payment method is "Bank Slip"
*
* @return bool
*/
public function is_bank_slip()
{
return 'bank_slip' === $this->gateway->type();
}
/**
* Retrieve payment method code
*
* @return string Vindi payment method code
*/
public function payment_method_code()
{
switch ($this->gateway->type()) {
case 'cc':
$code = 'credit_card';
break;
case 'bolepix':
$code = 'pix_bank_slip';
break;
default:
$code = $this->gateway->type();
break;
}
return $code;
}
/**
* Interrupt the payment process and throw an error if needed.
* Log the message, add it to the order note and send an alert to the user
*
* @param string $message The error message
* @param bool $throw_exception When true an exception is thrown
*
* @return bool Always returns false
*
* @throws Exception
*/
public function abort($message, $throw_exception = false)
{
$this->order->add_order_note($message);
wc_add_notice($message, 'error');
if ($throw_exception) {
throw new Exception($message);
}
return false;
}
/**
* Check if the order type is valid and process it.
*
* @return array|void
*
* @throws Exception
*/
public function process()
{
switch ($orderType = $this->get_order_type()) {
case static::ORDER_TYPE_SINGLE:
case static::ORDER_TYPE_SUBSCRIPTION:
return $this->process_order();
case static::ORDER_TYPE_INVALID:
default:
return $this->abort(__('Falha ao processar carrinho de compras. Verifique os itens escolhidos e tente novamente.', VINDI), true);
}
}
/**
* Process current order.
*
* @return array
*
* @throws Exception
*/
public function process_order()
{
$this->check_trial_and_single_product();
$customer = $this->get_customer();
$order_items = $this->order->get_items();
$bills = [];
$order_post_meta = [];
$bill_products = [];
$subscription_products = [];
$subscriptions_ids = [];
$wc_subscriptions_ids = [];
$subscriptions_grouped_by_period = array();
foreach ($order_items as $order_item) {
$product = $order_item->get_product();
if ($this->is_subscription_type($product)) {
$product_id = $product->get_id();
if ($this->is_variable($product)) {
$product_id = $order_item['variation_id'];
}
$period = get_post_meta($product_id, '_subscription_period', true);
$interval = get_post_meta($product_id, '_subscription_period_interval', true);
$subscriptions_grouped_by_period[$period . $interval][] = $order_item;
array_push($subscription_products, $order_item);
continue;
}
array_push($bill_products, $order_item);
}
$this->check_multiple_subscriptions_of_same_period($subscriptions_grouped_by_period);
foreach ($subscription_products as $subscription_order_item) {
if (empty($subscription_order_item))
continue;
try {
$subscription = $this->create_subscription($customer['id'], $subscription_order_item);
$subscription_id = $subscription['id'];
$wc_subscription_id = $subscription['wc_id'];
array_push($subscriptions_ids, $subscription_id);
array_push($wc_subscriptions_ids, $wc_subscription_id);
$subscription_bill = $subscription['bill'];
$order_post_meta[$subscription_id]['cycle'] = $subscription['current_period']['cycle'];
$order_post_meta[$subscription_id]['product'] = $subscription_order_item->get_product()->get_name();
$order_post_meta[$subscription_id]['bill'] = $this->create_bill_meta_for_order($subscription_bill);
$bills[] = $subscription['bill'];
$message = $this->cancel_if_denied_bill_status($subscription['bill']);
if ($message) {
throw new Exception($message);
}
update_post_meta($wc_subscription_id, 'vindi_subscription_id', $subscription_id);
continue;
} catch (Exception $err) {
$message = $err->getMessage();
$this->cancel_subscriptions_and_order($wc_subscriptions_ids, $subscriptions_ids, $message);
}
}
if (!empty($bill_products)) {
try {
$single_payment_bill = $this->create_bill($customer['id'], $bill_products);
$order_post_meta['single_payment']['product'] = 'Produtos Avulsos';
$order_post_meta['single_payment']['bill'] = $this->create_bill_meta_for_order($single_payment_bill);
$bills[] = $single_payment_bill;
$message = $this->cancel_if_denied_bill_status($single_payment_bill);
if ($message) {
$this->order->update_status('cancelled', __($message, VINDI));
if ($subscriptions_ids) {
$this->suspend_subscriptions($subscriptions_ids);
}
$this->cancel_bills($bills, __('Algum pagamento do pedido não pode ser processado', VINDI));
$this->abort(__($message, VINDI), true);
}
} catch (Exception $err) {
$this->logger->log(sprintf('Deu erro na criação da conta %s', $single_payment_bill));
$this->abort(__('Não foi possível criar o pedido.', VINDI), true);
}
}
$this->order->update_meta_data('vindi_order', $order_post_meta);
$this->order->save();
WC()->session->__unset('current_payment_profile');
WC()->session->__unset('current_customer');
remove_action('woocommerce_scheduled_subscription_payment', 'WC_Subscriptions_Manager::prepare_renewal');
return $this->finish_payment($bills);
}
private function check_trial_and_single_product()
{
if ($this->order_has_trial_and_simple_product()) {
$message = __('Não é possível comprar produtos simples e assinaturas com trial no mesmo pedido!', VINDI);
$this->order->update_status('failed', $message);
wc_add_notice($message, 'error');
throw new Exception($message);
}
}
private function check_multiple_subscriptions_of_same_period($subscriptions_grouped_by_period)
{
foreach ($subscriptions_grouped_by_period as $subscription_group) {
if (count($subscription_group) > 1) {
$msg = 'Não é permitido criar um único pedido com múltiplas assinaturas de mesma periodicidade';
$this->abort(__($msg, VINDI), true);
}
}
}
/**
* Create a payment profile for the customer
*
* @param int $customer_id Vindi customer id
*
* @throws Exception
*/
protected function create_payment_profile($customer_id)
{
$cc_info = $this->get_cc_payment_type($customer_id);
if (false === $cc_info) {
return;
}
$payment_profile = $this->routes->createCustomerPaymentProfile($cc_info);
if (!$payment_profile) {
$this->abort(__('Falha ao registrar o método de pagamento. Verifique os dados e tente novamente.', VINDI), true);
}
if ($this->gateway->verify_method()) {
$this->verify_payment_profile($payment_profile['id']);
}
}
/**
* Check if the payment profile is valid
*
* @param int $payment_profile_id The customer's payment profile id
*
* @throws Exception
*/
protected function verify_payment_profile($payment_profile_id)
{
if (!$this->routes->verifyCustomerPaymentProfile($payment_profile_id)) {
$this->abort(__('Não foi possível realizar a verificação do seu cartão de crédito!', VINDI), true);
}
}
/**
* Get the subscription/product expiration time in months
*
* @param WC_Order_Item_Product $item
*
* @return int
*/
private function get_cycle_from_product_type($item)
{
$cycles = null;
$product = is_object($item) && method_exists($item, 'get_product') ? $item->get_product() : false;
if ($item['type'] == 'sign_up_fee') {
return 1;
}
if ($item['type'] == 'shipping' || $item['type'] == 'tax') {
if ($this->vindi_settings->get_shipping_and_tax_config()) {
return 1;
}
return $this->single_freight ? 1 : null;
} elseif (!$product || !$this->is_subscription_type($product)) {
return 1;
}
if ($product) {
$cycles = (int) get_post_meta($product->get_id(), '_subscription_length', true);
}
return $cycles > 0 ? $cycles : null;
}
/**
* Check if the product needs to be shipped only once
*
* @param WC_Product $product Woocommerce product
*
* @return bool
*/
private function is_one_time_shipping($product)
{
return get_post_meta($product->get_id(), '_subscription_one_time_shipping', true) == 'yes';
}
/**
* Build the array of product(s), shipping, tax and discounts to send to Vindi
*
* @param string $order_type Order type. Possible values 'bill' and 'subscription', defaults to 'bill'
* @param WC_Order_Item_Product|WC_Order_Item_Product[] $product The product to be built. If the order type is 'bill' this will be an array of WC_Order_Item_Product,
* if it's 'subscription' it will be a single WC_Order_Item_Product.
*
* @return array
*
* @throws Exception
*/
public function build_product_items($product, $order_type = 'bill')
{
if (!$product) {
$this->abort(__("Ocorreu um erro ao gerar o seu pedido!", VINDI), true);
}
$call_build_items = $this->get_call_build_items_method($order_type);
$product_items = [];
$order_items = [];
$order_items = $this->add_additional_items($order_items, $order_type, $product);
$product_items = $this->build_items($order_items, $call_build_items);
if (empty($product_items)) {
return $this->abort(__('Falha ao recuperar informações sobre o produto na Vindi. Verifique os dados e tente novamente.', VINDI), true);
}
$new_item = $this->calculate_discount($product_items);
return $new_item;
}
protected function get_call_build_items_method($order_type)
{
$call_build_items = "build_product_items_for_{$order_type}";
if (false === method_exists($this, $call_build_items)) {
$this->abort(__("Ocorreu um erro ao gerar o seu pedido!", VINDI), true);
}
return $call_build_items;
}
protected function add_additional_items($order_items, $order_type, $product)
{
$order_items = [];
if ('bill' === $order_type) {
$order_items = $this->build_product_from_order_item($order_type, $product);
} else {
$order_items[] = $this->build_product_from_order_item($order_type, $product);
}
$order_items[] = $this->build_shipping_item($order_items);
$order_items[] = $this->build_tax_item($order_items);
$order_items[] = $this->build_sign_up_fee_item($order_items);
if ('bill' === $order_type) {
$order_items[] = $this->build_discount_item_for_bill($order_items);
}
$order_items[] = $this->build_interest_rate_item($order_items);
return $order_items;
}
protected function build_items($order_items, $call_build_items)
{
$product_items = [];
foreach ($order_items as $order_item) {
if (!empty($order_item)) {
$newProduct = $this->$call_build_items($order_item);
$product_items[] = $newProduct;
}
}
return $product_items;
}
protected function calculate_discount($order_items)
{
$new_order_items = [];
$remainder = 0;
$item = $this->routes->findOrCreateProduct("[WC] Taxa de adesão", "WC-SUF");
$taxa_id = array(
'vindi_id' => $item['id']
);
foreach ($order_items as $order_item) {
$new_order_item = $order_item;
$total_discount = 0;
$full_price = $this->calculate_full_price($order_item);
$remainder = $this->apply_discount($order_item, $full_price, $remainder);
if ($order_item['product_id'] === $taxa_id['vindi_id']) {
$total_discount = $this->validate_discount_percentage_sign_up_fee($order_item);
$remainder = $this->apply_remainder($remainder, $full_price, $new_order_item);
}
if ($total_discount > 0) {
$new_order_item['discounts'][] = array(
'discount_type' => 'amount',
'amount' => $total_discount,
'cycles' => 1
);
}
$new_order_items[] = $new_order_item;
}
return $new_order_items;
}
protected function validate_discount_percentage_sign_up_fee($order_item)
{
$coupons = array_values($this->vindi_settings->woocommerce->cart->get_coupons());
$total_discount = 0;
foreach ($coupons as $coupon) {
$discount_type = $coupon->get_discount_type();
if ($discount_type === 'percent') {
$discount_amount = $coupon->get_amount();
$price = $order_item['pricing_schema']['price'];
$quantity = $order_item['quantity'];
$discount = abs(($price * $quantity)) * $discount_amount / 100;
$total_discount += $discount;
}
}
return $total_discount;
}
protected function calculate_full_price($order_item)
{
$price = isset($order_item['pricing_schema']['price']) ? abs($order_item['pricing_schema']['price']) : 0;
$quantity = isset($order_item['quantity']) ? $order_item['quantity'] : 1;
$total_price = $price * $quantity;
return $total_price;
}
protected function apply_discount($order_item, $full_price, $remainder)
{
if (isset($order_item['discounts'])) {
$discountTotal = array_reduce($order_item['discounts'], function ($total, $discount) {
if (isset($discount['amount'])) {
return $total + $discount['amount'];
}
return $total;
}, 0);
if ($discountTotal > $full_price) {
$remainder = $discountTotal - $full_price;
}
}
return $remainder;
}
protected function apply_remainder($remainder, $full_price, &$new_order_item)
{
if ($remainder > 0) {
$amount_to_apply = min($remainder, $full_price);
$new_order_item['discounts'][] = array(
'discount_type' => 'amount',
'amount' => $amount_to_apply,
'cycles' => 1
);
$remainder -= $amount_to_apply;
}
return $remainder;
}
/**
* Retrives the product(s) information. Adds the vindi_id, the type and the price to it.
*
* @param string $order_type ('subscription' or 'bill')
* @param WC_Order_Item_Product|WC_Order_Item_Product[] $order_items. Subscriptions will pass only one order_item and
* Bills will pass an array with all the products to be processed
*
* @return array
*/
protected function build_product_from_order_item($order_type, $order_items)
{
if ('bill' === $order_type) {
$order_items = $this->build_product_from_order_item_bill($order_items);
return $order_items;
}
$product = $order_items->get_product();
$order_items['type'] = 'product';
$product_id = $product->get_id();
if ($this->is_variable($product)) {
$product_id = $order_items['variation_id'];
}
$get_vindi = $this->get_vindi_code($product_id);
$order_items['vindi_id'] = $get_vindi ? $get_vindi : $product->vindi_id;
if ($this->subscription_has_trial($product)) {
$matching_item = $this->get_trial_matching_subscription_item($order_items);
$order_items['price'] = (float) $matching_item['subtotal'] / $matching_item['qty'];
} else {
$order_items['price'] = (float) $order_items['subtotal'] / $order_items['qty'];
}
return $order_items;
}
protected function build_product_from_order_item_bill($order_items)
{
foreach ($order_items as $key => $order_item) {
$product = $order_item->get_product();
$order_items[$key]['type'] = 'product';
$product_id = $product->get_id();
if ($this->is_variable($product)) {
$product_id = $order_item['product_id'];
}
$vindi_id = get_post_meta($product_id, 'vindi_product_id', true);
if (!$vindi_id) {
$vindi_id = $this->routes->findProductByCode('WC-' . $product_id)['id'];
}
$order_items[$key]['vindi_id'] = $vindi_id;
$order_items[$key]['price'] = (float) $order_items[$key]['subtotal'] / $order_items[$key]['qty'];
}
return $order_items;
}
/**
* Create the sign-up fee item to be added to the bill.
*
* @param WC_Order_Item_Product[] $order_items. Array with all items to add
* the respective delivered value
*
* @return array
*/
protected function build_sign_up_fee_item($order_items)
{
foreach ($order_items as $order_item) {
if (!is_object($order_item) || !method_exists($order_item, 'get_product')) {
continue;
}
$product = $order_item->get_product();
$sign_up_fee = $product->get_meta('_subscription_sign_up_fee');
if ($sign_up_fee != null && $sign_up_fee > 0) {
$item = $this->routes->findOrCreateProduct("[WC] Taxa de adesão", "WC-SUF");
$sign_up_fee_item = array(
'type' => 'sign_up_fee',
'vindi_id' => $item['id'],
'price' => (float) $sign_up_fee,
'qty' => $order_item['quantity'],
);
$order_item['price'] -= $sign_up_fee;
return $sign_up_fee_item;
}
}
}
/**
* Create the shipping item to be added to the bill.
*
* @param WC_Order_Item_Product[] $order_items. Array with all items to add
* the respective delivered value // TODO.
*
* @return array
*/
protected function build_interest_rate_item($order_items)
{
$interest_rate_item = [];
if (!($this->is_cc() && $this->installments() > 1 && $this->gateway->is_interest_rate_enabled())) {
return $interest_rate_item;
}
$interest_rate = $this->gateway->get_interest_rate();
$installments = $this->installments();
$cart = WC()->cart;
$cart_total = $cart->total;
foreach ($cart->get_fees() as $index => $fee) {
if ($fee->name == __('Juros', VINDI)) {
$cart_total -= $fee->amount;
}
}
$total_price = $cart_total * (1 + (($interest_rate / 100) * ($installments - 1)));
$interest_price = (float) $total_price - $cart_total;
$item = $this->routes->findOrCreateProduct("Juros", 'wc-interest-rate');
$interest_rate_item = array(
'type' => 'interest_rate',
'vindi_id' => $item['id'],
'price' => $interest_price,
'qty' => 1,
);
return $interest_rate_item;
}
/**
* Create the shipping item to be added to the bill.
*
* @param WC_Order_Item_Product[] $order_items. Array with all items to add
* the respective delivered value // TODO.
*
* @return array
*/
protected function build_shipping_item($order_items)
{
$shipping_item = [];
$shipping_method = $this->order->get_shipping_method();
$get_total_shipping = $this->order->get_shipping_total();
if (empty($shipping_method)) {
return $shipping_item;
}
foreach ($order_items as $order_item) {
$wc_subscription = VindiHelpers::get_matching_subscription($this->order, $order_item);
$product = $order_item->get_product();
$one_time_shipping = $this->is_one_time_shipping($product) && $this->single_freight ? true : false;
if ($this->is_subscription_type($product) && !$one_time_shipping) {
$shipping_method = $wc_subscription->get_shipping_method();
$get_total_shipping = $wc_subscription->get_total_shipping();
}
if ($product->needs_shipping()) {
$item = $this->create_shipping_product($shipping_method);
$shipping_item = array(
'type' => 'shipping',
'vindi_id' => $item['id'],
'price' => $get_total_shipping,
'qty' => 1,
);
}
}
return $shipping_item;
}
private function create_shipping_product($shipping_method)
{
return $this->routes->findOrCreateProduct(
sprintf("Frete (%s)", $shipping_method),
sanitize_title($shipping_method)
);
}
/**
* Create the tax item.
*
* @param WC_Order_Item_Product[] $order_items. Products to calculate the tax amount
*
* @return array
*/
protected function build_tax_item($order_items)
{
$total_tax = 0;
$taxItem = [];
foreach ($order_items as $order_item) {
if (get_option('woocommerce_tax_total_display') === 'itemized') {
if (!empty($order_item['type'])) {
if ($order_item['type'] === 'shipping') {
$total_tax += (float) ($this->order->get_shipping_tax());
} else {
$total_tax += (float) ($order_item->get_total_tax());
}
}
} else {
!empty($order_item['type']) && $total_tax += ($order_item['type'] === 'shipping' ? (float) $this->order->get_shipping_tax() : (float) $order_item->get_total_tax());
}
}
if ($total_tax > 0) {
$item = $this->routes->findOrCreateProduct("Taxa", 'wc-tax');
$taxItem = array(
'type' => 'tax',
'vindi_id' => $item['id'],
'price' => (float) $total_tax,
'qty' => 1,
);
}
return $taxItem;
}
/**
* Create discount item for a bill.
*
* @param WC_Order_Item_Product[] $order_items. All the products to calculate
* the discount amount
*
* @return array
*/
protected function build_discount_item_for_bill($order_items)
{
$discount_item = [];
$bill_total_discount = 0;
foreach ($order_items as $order_item) {
if (isset($order_item['subtotal']) && isset($order_item['total'])) {
$bill_total_discount += (float) ($order_item['subtotal'] - $order_item['total']);
}
}
if (empty($bill_total_discount)) {
return $discount_item;
}
$item = $this->routes->findOrCreateProduct("Cupom de desconto", 'wc-discount');
$discount_item = array(
'type' => 'discount',
'vindi_id' => $item['id'],
'price' => (float) $bill_total_discount * -1,
'qty' => 1,
);
return $discount_item;
}
/**
* Create bill product item to send to the Vindi API.
*
* @param WC_Order_Item_Product $order_item. The product to be converted to
* the correct product format.
*
* @return array
*/
protected function build_product_items_for_bill($order_item)
{
$item = array(
'product_id' => $order_item['vindi_id'],
'quantity' => $order_item['qty'],
'pricing_schema' => array(
'price' => $order_item['price'],
'schema_type' => 'per_unit',
),
);
if (
'discount' == $order_item['type'] || 'shipping' == $order_item['type'] ||
'tax' == $order_item['type'] || 'interest_rate' == $order_item['type'] || 'sign_up_fee' == $order_item['type']
) {
$item = array(
'product_id' => $order_item['vindi_id'],
'amount' => $order_item['price'],
);
}
return $item;
}
/**
* Create subscription product item to send to the Vindi API.
*
* @param WC_Order_Item_Product $order_item. The product to be converted to
* the correct product format.
*
* @return array
*/
protected function build_product_items_for_subscription($order_item)
{
$plan_cycles = $this->get_cycle_from_product_type($order_item);
$product_item = array(
'product_id' => $order_item['vindi_id'],
'quantity' => $order_item['qty'],
'cycles' => $plan_cycles,
'pricing_schema' => array(
'price' => abs($order_item['price']),
'schema_type' => 'per_unit',
),
);
$coupons = array_values($this->vindi_settings->woocommerce->cart->get_coupons());
if (!empty($coupons) && $order_item['type'] == 'line_item') {
$product_item['discounts'] = [];
foreach ($coupons as $coupon) {
if ($this->coupon_supports_product($order_item, $coupon)) {
$discount_item = $this->build_discount_item_for_subscription($coupon, $order_item, $plan_cycles);
$product_item['discounts'][] = $discount_item;
}
}
}
return $product_item;
}
/**
* Verify that the coupon can be applied to the current product.
*
* @param WC_Order_Item_Product $order_item. The product.
* @param WC_Coupon $coupon. The coupon.
*
* @return bool
*/
protected function coupon_supports_product($order_item, $coupon)
{
$product_id = $order_item->get_product()->get_id();
$included_products = $coupon->get_product_ids();
$excluded_products = $coupon->get_excluded_product_ids();
if (!empty($excluded_products)) {
if (in_array($product_id, $excluded_products)) {
// The coupon doesn't support the current product
return false;
}
}
if (!empty($included_products)) {
if (!in_array($product_id, $included_products)) {
// The coupon doesn't support the current product
return false;
}
}
return true;
}
/**
* Create a discount item for a subscription.
*
* @param WC_Coupon $coupon. The coupon to be added.
* @param int $plan_cycles. The amount of cycles that the subscription has.
*
* @return array
*/
protected function build_discount_item_for_subscription($coupon, $order_item, $plan_cycles = 0)
{
$discount_item = [];
$amount = $coupon->get_amount();
$discount_type = $coupon->get_discount_type();
if ($discount_type == 'fixed_cart') {
$total_cart = WC()->cart->subtotal;
$percentage_item = $order_item['subtotal'] / $total_cart;
$discount_value = $percentage_item * $amount;
$discount_item['discount_type'] = 'amount';
$discount_item['amount'] = (float) $discount_value;
$discount_item['cycles'] = 1;
return $discount_item;
} elseif ($discount_type == 'fixed_product') {
$discount_value = $order_item['quantity'] * $amount;
$discount_item['discount_type'] = 'amount';
$discount_item['amount'] = (float) $discount_value;
} elseif (strpos($discount_type, 'fixed') !== false) {
$discount_item['discount_type'] = 'amount';
$discount_item['amount'] = (float) $amount;
} elseif (strpos($discount_type, 'percent') !== false ||strpos($discount_type, 'recurring_percent') !== false) {
$discount_item['discount_type'] = 'amount';
$discount_item['amount'] = abs($amount / 100 * ($order_item['price'] * $order_item['quantity']));
}
$discount_item['cycles'] = $this->config_discount_cycles($coupon, $plan_cycles);
return $discount_item;
}
/**
* Configure the discount cycles that the coupon will be used.
*
* @param WC_Coupon $coupon. The coupon to be added.
* @param int $plan_cycles. The amount of cycles that the subscription has.
*
* @return int
*/
protected function config_discount_cycles($coupon, $plan_cycles = 0)
{
$cycle_count = get_post_meta($coupon->get_id(), 'cycle_count', true);
if ($coupon->get_discount_type() == 'recurring_percent') {
$subscriptions_coupon = new WC_Subscriptions_Coupon();
$cycle_count = $subscriptions_coupon->get_coupon_limit($coupon->get_id());
}
if ($cycle_count == 0) {
return null;
}
return $this->get_plan_length($cycle_count, $plan_cycles);
}
/**
* Get plan length
*
* @param int $cycle_count
* @param int $plan_cycles
*
* @return int
*/
private function get_plan_length($cycle_count, $plan_cycles)
{
if (!$cycle_count) {
return null;
}
if ($plan_cycles) {
return min($plan_cycles, $cycle_count);
}
return $cycle_count;
}
/**
* Retrieve number of installments from order.
* If the order contains subscriptions the return will be 1,
* else it will be the amount selected by the user during checkout.
*
* @return int
*/
protected function installments()
{
if (
'credit_card' == $this->payment_method_code() &&
isset($_POST['vindi_cc_installments'])
) {
return filter_var($_POST['vindi_cc_installments'], FILTER_SANITIZE_NUMBER_INT);
}
return 1;
}
/**
* Retrieve Plan for Vindi Subscription.
*
* @param WC_Order_Item_Product $order_item
*
* @return int|bool
*/
public function get_plan_from_order_item($order_item)
{
$product = $order_item->get_product();
if (isset($order_item['variation_id']) && $order_item['variation_id'] != 0) {
$vindi_plan = get_post_meta($order_item['variation_id'], 'vindi_plan_id', true);
if (empty($vindi_plan) || !is_numeric($vindi_plan) || is_null($vindi_plan) || $vindi_plan == 0) {
$vindi_plan = get_post_meta($product->get_id(), 'vindi_plan_id', true);
}
} else {
$vindi_plan = get_post_meta($product->get_id(), 'vindi_plan_id', true);
}
if ($this->is_subscription_type($product) and !empty($vindi_plan)) {
return $vindi_plan;
}
$this->abort(__('O produto selecionado não é uma assinatura.', VINDI), true);
}
/**
* Create a subscription within Vindi
*
* @param int $customer_id ID of the customer that placed the order
* @param WC_Order_item_product $order_item Item to add to the subscription.
*
* @return array
* @throws Exception
*/
protected function create_subscription($customer_id, $order_item)
{
if ($order_item == null || empty($order_item)) {
return;
}
$data = [];
$data['customer_id'] = $customer_id;
$data['payment_method_code'] = $this->payment_method_code();
$data['installments'] = $this->installments();
$data['product_items'] = array();
$product = $order_item->get_product();
if ($this->is_subscription_type($product) || $this->is_variable($product)) {
$data['plan_id'] = $this->get_plan_from_order_item($order_item);
$wc_subscription_id = VindiHelpers::get_matching_subscription($this->order, $order_item)->get_id();
$data['code'] = strpos($wc_subscription_id, 'WC') > 0 ? $wc_subscription_id : 'WC-' . $wc_subscription_id;
}
$data['product_items'] = $this->get_build_products($data, $order_item);
$subscription = $this->routes->createSubscription($data);
if (!isset($subscription['id']) || empty($subscription['id'])) {
throw new Exception(sprintf(__('Pagamento Falhou. (%s)', VINDI), $this->vindi_settings->api->last_error));
}
$subscription['wc_id'] = $wc_subscription_id;
if (isset($subscription['bill']['id'])) {
$this->order->update_meta_data('vindi_bill_id', $subscription['bill']['id']);
$this->order->save();
}
return $subscription;
}
private function get_build_products($data, $order_item)
{
return array_merge(
$data['product_items'],
$this->build_product_items($order_item, 'subscription')
);
}
/**
* Cancel subscriptions and order in case of error on payment
*
* @param array $wc_subscriptions_ids Array with the IDs of woocommerce subscriptions that must be canceled
* @param array $subscriptions_ids Array with the IDs of vindi subscriptions that must be suspended
* @param string $message Error message
*/
private function cancel_subscriptions_and_order($wc_subscriptions_ids, $subscriptions_ids, $message)
{
$this->suspend_subscriptions($subscriptions_ids);
foreach ($wc_subscriptions_ids as $wc_subscription_id) {
$wc_subscription = wcs_get_subscription($wc_subscription_id);
$wc_subscription->update_status('cancelled', __($message, VINDI));
}
$this->order->update_status('cancelled', __($message, VINDI));
if ($message) {
$this->abort(__(sprintf('Erro ao criar o pedido: %s', $message), VINDI), true);
}
}
/**
* Create a bill within Vindi
*
* @param int $customer_id ID of the customer that placed the order
* @param WC_Order_item_product[] $order_items Array with items to add to the bill.
*
* @return int
* @throws Exception
*/
protected function create_bill($customer_id, $order_items)
{
$data = array(
'customer_id' => $customer_id,
'payment_method_code' => $this->payment_method_code(),
'bill_items' => $this->build_product_items($order_items, 'bill'),
'code' => $this->order->get_id(),
'installments' => $this->installments(),
);
$bill = $this->routes->createBill($data);
if (!$bill) {
$this->logger->log(sprintf('Erro no pagamento do pedido %s.', $this->order->get_id()));
$message = sprintf(__('Pagamento Falhou. (%s)', VINDI), $this->vindi_settings->api->last_error);
$this->order->update_status('failed', $message);
throw new Exception($message);
}
if ($bill['id']) {
$this->logger->log(sprintf('Update Bill: %s', json_encode($bill)));
$this->order->save();
}
return $bill;
}
/**
* Create bill meta array to add to the order
*
* @param array $bill The bill returned from Vindi API
*
* @return array
*/
protected function create_bill_meta_for_order($bill)
{
$bill_meta = [];
$bill_meta['id'] = $bill['id'];
$bill_meta['status'] = $bill['status'];
if (isset($bill['charges']) && count($bill['charges'])) {
$charges = end($bill['charges']);
$bill_meta['bank_slip_url'] = $charges['print_url'] ?? '';
if (
array_intersect([$this->payment_method_code()], ['pix', 'pix_bank_slip'])
&& isset($charges['last_transaction']['gateway_response_fields'])
) {
$transaction = $charges['last_transaction']['gateway_response_fields'];
$bill_meta['charge_id'] = $charges['id'];
$bill_meta['pix_expiration'] = $transaction['max_days_to_keep_waiting_payment'] ?? '';
$bill_meta['pix_code'] = $transaction['qrcode_original_path'];
$bill_meta['pix_qr'] = $transaction['qrcode_path'];
}
}
return $bill_meta;
}
/**
* Check if bill was rejected by Vindi
*
* @param array $bill The bill returned from Vindi API
*
* @return bool|string
*/
protected function cancel_if_denied_bill_status($bill)
{
if (empty($bill['charges'])) {
return false;
}
$last_charge = end($bill['charges']);
$transaction_status = $last_charge['last_transaction']['status'];
$denied_status = [
'rejected' => 'Não foi possível processar seu pagamento. Por favor verifique os dados informados. ',
'failure' => 'Ocorreu um erro ao tentar aprovar a transação, tente novamente.'
];
if (array_key_exists($transaction_status, $denied_status)) {
if ($this->is_cc() && $last_charge['last_transaction']['gateway_message'] != null) {
return $denied_status[$transaction_status] . $last_charge['last_transaction']['gateway_message'];
}
return $denied_status[$transaction_status];
}
return false;
}
/**
* Suspend subscription within Vindi
*
* @param array $subscriptions_ids Array with the IDs of subscriptions that were processed
*/
protected function suspend_subscriptions($subscriptions_ids)
{
foreach ($subscriptions_ids as $subscription_id) {
$this->routes->suspendSubscription($subscription_id, true);
}
}
/**
* Suspend bills within Vindi
*
* @param array $bills Array with the bills that were processed
*/
protected function cancel_bills($bills, $comments = '')
{
foreach ($bills as $bill) {
$this->routes->deleteBill($bill['id'], $comments);
}
}
/**
* Finish the payment
*
* @param array $bills Order bills returned from Vindi API
*
* @return array
*/
protected function finish_payment($bills)
{
$this->vindi_settings->woocommerce->cart->empty_cart();
$bills_status = [];
foreach ($bills as $bill) {
array_push($bills_status, $bill['status']);
$this->generate_log_message($bill);
}
$this->update_order_status($bills_status);
return array(
'result' => 'success',
'redirect' => $this->order->get_checkout_order_received_url(),
);
}
private function generate_log_message($bill)
{
$fatura = $bill['id'];
$pedido = $this->order->get_id();
$message = 'Aguardando pagamento da fatura %s do pedido %s pela Vindi.';
if ($bill['status'] == 'paid') {
$message = 'O Pagamento da fatura %s do pedido %s foi realizado com sucesso pela Vindi.';
}
$this->logger->log(sprintf($message, $fatura, $pedido));
}
private function update_order_status($bills_status)
{
$status = 'pending';
$status_message = '';
if (sizeof($bills_status) == sizeof(array_keys($bills_status, 'paid'))) {
$status = $this->vindi_settings->get_return_status();
}
if ($this->order_has_trial()) {
$status = $this->vindi_settings->get_return_status();
$status_message = __('Aguardando pagamento do pedido.', VINDI);
}
$this->order->update_status($status, $status_message);
}
/**
* Find or create the product within Vindi
* and add the vindi id to the product array
*
* @param WC_Order_Item_Product $order_item
*
* @return WC_Product Woocommerce product array with a vindi id
*/
protected function get_product($order_item)
{
$product = $order_item->get_product();
$product_id = $order_item->get_id();
$vindi_product_id = get_post_meta($product, 'vindi_product_id', true);
if (!$vindi_product_id) {
$vindi_product = 63;
if (!$this->is_subscription_type($product)) {
$vindi_product = $this->controllers->products->create($product_id, '', '', true);
} else {
$vindi_product = $this->controllers->plans->create($product_id, '', '', true);
}
$vindi_product_id = $vindi_product['id'];
}
if (empty($vindi_product_id) || !$vindi_product_id) {
$vindi_product_id = $this->routes->findProductByCode('WC-' . $product->id)['id'];
}
$product->vindi_id = (int) $vindi_product_id > 0 ? $vindi_product_id : 63;
if ($product->id === null) $product->id = 63;
return $product;
}
/**
* Check if the order has a subscription with trial and simple products.
*
* @since 1.0.0
* @return bool
*/
public function order_has_trial_and_simple_product()
{
$has_trial = false;
$has_simple_product = false;
$order_items = $this->order->get_items();
foreach ($order_items as $order_item) {
$product = $order_item->get_product();
if ($this->subscription_has_trial($product)) {
$has_trial = true;
if ($has_simple_product) {
return true;
}
} else {
$has_simple_product = true;
if ($has_trial) {
return true;
}
}
}
return $has_trial && $has_simple_product;
}
/**
* Check if the product is variable
*
* @param WC_Product $product
* @return bool
*/
protected function is_variable(WC_Product $product)
{
return (bool) preg_match('/variation/', $product->get_type());
}
/**
* Check if the product is a subscription
*
* @param WC_Product $product
* @return bool
*/
protected function is_subscription_type(WC_Product $product)
{
return (bool) preg_match('/subscription/', $product->get_type());
}
/**
* Check if the subscription has a trial period
*
* @param WC_Product $product
* @return bool
*/
protected function subscription_has_trial(WC_Product $product)
{
return $this->is_subscription_type($product) && class_exists('WC_Subscriptions_Product') && WC_Subscriptions_Product::get_trial_length($product->get_id()) > 0;
}
/**
* Check if the order has a subscription with trial period
*
* @return bool
*/
protected function order_has_trial()
{
$has_trial = false;
$order_items = $this->order->get_items();
foreach ($order_items as $order_item) {
$product = $order_item->get_product();
if ($this->subscription_has_trial($product)) {
$has_trial = true;
}
}
return $has_trial;
}
/**
* Get trial item quantity, subtotal and total price.
*
* @param WC_Order_Item_Product $order_item
* @return WC_Order_Item_Product
*/
protected function get_trial_matching_subscription_item(WC_Order_Item_Product $order_item)
{
$subscription = VindiHelpers::get_matching_subscription($this->order, $order_item);
$matching_item = VindiHelpers::get_matching_subscription_item($subscription, $order_item);
return $matching_item;
}
protected function get_vindi_code(String $product)
{
try {
$response = $this->routes->findProductByCode('WC-' . $product);
return $response['id'];
} catch (Exception $err) {
return $product;
}
}
}