File "Orders.php"

Full Path: /home/warrior1/public_html/plugins/facebook-for-woocommerce/includes/Commerce/Orders.php
File size: 26.51 KB
MIME-type: text/x-php
Charset: utf-8

<?php
// phpcs:ignoreFile
/**
 * Copyright (c) Facebook, Inc. and its affiliates. All Rights Reserved
 *
 * This source code is licensed under the license found in the
 * LICENSE file in the root directory of this source tree.
 *
 * @package FacebookCommerce
 */

namespace SkyVerge\WooCommerce\Facebook\Commerce;

use SkyVerge\WooCommerce\Facebook\API\Orders\Cancel\Request as Cancellation_Request;
use SkyVerge\WooCommerce\Facebook\API\Orders\Order;
use SkyVerge\WooCommerce\Facebook\Products;
use SkyVerge\WooCommerce\Facebook\API\Orders\Refund\Request as Refund_Request;
use SkyVerge\WooCommerce\Facebook\Utilities;
use SkyVerge\WooCommerce\PluginFramework\v5_10_0\SV_WC_API_Exception;
use SkyVerge\WooCommerce\PluginFramework\v5_10_0\SV_WC_Plugin_Exception;

defined( 'ABSPATH' ) or exit;

/**
 * General Commerce orders handler.
 *
 * @since 2.1.0
 */
class Orders {


	/** @var string the fetch orders action */
	const ACTION_FETCH_ORDERS = 'wc_facebook_commerce_fetch_orders';

	/** @var string the meta key used to store the remote order ID */
	const REMOTE_ID_META_KEY = '_wc_facebook_commerce_remote_id';

	/** @var string the meta key used to store the email remarketing option */
	const EMAIL_REMARKETING_META_KEY = '_wc_facebook_commerce_email_remarketing';

	/** @var string buyer's remorse refund reason */
	const REFUND_REASON_BUYERS_REMORSE = 'BUYERS_REMORSE';

	/** @var string damaged goods refund reason */
	const REFUND_REASON_DAMAGED_GOODS = 'DAMAGED_GOODS';

	/** @var string not as described refund reason */
	const REFUND_REASON_NOT_AS_DESCRIBED = 'NOT_AS_DESCRIBED';

	/** @var string quality issue refund reason */
	const REFUND_REASON_QUALITY_ISSUE = 'QUALITY_ISSUE';

	/** @var string wrong item refund reason */
	const REFUND_REASON_WRONG_ITEM = 'WRONG_ITEM';

	/** @var string other refund reason */
	const REFUND_REASON_OTHER = 'REFUND_REASON_OTHER';

	/** @var string customer requested cancellation */
	const CANCEL_REASON_CUSTOMER_REQUESTED = 'CUSTOMER_REQUESTED';

	/** @var string out of stock cancellation */
	const CANCEL_REASON_OUT_OF_STOCK = 'OUT_OF_STOCK';

	/** @var string invalid address cancellation */
	const CANCEL_REASON_INVALID_ADDRESS = 'INVALID_ADDRESS';

	/** @var string suspicious order cancellation */
	const CANCEL_REASON_SUSPICIOUS_ORDER = 'SUSPICIOUS_ORDER';

	/** @var string other reason cancellation */
	const CANCEL_REASON_OTHER = 'CANCEL_REASON_OTHER';


	/**
	 * Orders constructor.
	 *
	 * @since 2.1.0
	 */
	public function __construct() {

		$this->add_hooks();
	}


	/**
	 * Returns whether or not the order is a pending Commerce order.
	 *
	 * @since 2.1.0
	 *
	 * @param \WC_Order $order order object
	 * @return bool
	 */
	public static function is_order_pending( \WC_Order $order ) {

		return self::is_commerce_order( $order ) && 'pending' === $order->get_status();
	}


	/**
	 * Returns whether or not the order is a Commerce order.
	 *
	 * @since 2.1.0
	 *
	 * @param \WC_Order $order order object
	 * @return bool
	 */
	public static function is_commerce_order( \WC_Order $order ) {

		return in_array( $order->get_created_via(), array( 'instagram', 'facebook' ), true );
	}


	/**
	 * Finds a local order based on the Commerce ID stored in REMOTE_ID_META_KEY.
	 *
	 * @since 2.1.0
	 *
	 * @param string $remote_id Commerce order ID
	 * @return \WC_Order|null
	 */
	public function find_local_order( $remote_id ) {

		$orders = wc_get_orders(
			array(
				'limit'      => 1,
				'status'     => 'any',
				'meta_key'   => self::REMOTE_ID_META_KEY,
				'meta_value' => $remote_id,
			)
		);

		return ! empty( $orders ) ? current( $orders ) : null;
	}


	/**
	 * Creates a local WooCommerce order based on an Orders API order object.
	 *
	 * @since 2.1.0
	 *
	 * @param Order $remote_order Orders API order object
	 * @return \WC_Order
	 * @throws SV_WC_Plugin_Exception|\WC_Data_Exception
	 */
	public function create_local_order( Order $remote_order ) {

		$local_order = new \WC_Order();
		$local_order->set_created_via( $remote_order->get_channel() );
		$local_order->set_status( 'pending' );
		$local_order->save();

		$local_order = $this->update_local_order( $remote_order, $local_order );

		return $local_order;
	}


	/**
	 * Updates a local WooCommerce order based on an Orders API order object.
	 *
	 * @since 2.1.0
	 *
	 * @param Order     $remote_order Orders API order object
	 * @param \WC_Order $local_order local order object
	 * @return \WC_Order
	 * @throws SV_WC_Plugin_Exception|\WC_Data_Exception
	 */
	public function update_local_order( Order $remote_order, \WC_Order $local_order ) {

		$total_items_tax = 0;

		// add/update items
		foreach ( $remote_order->get_items() as $item ) {

			$product = Products::get_product_by_fb_product_id( $item['product_id'] );

			if ( empty( $product ) ) {
				$product = Products::get_product_by_fb_retailer_id( $item['retailer_id'] );
			}

			if ( ! $product instanceof \WC_Product ) {

				// add a note and skip this item
				$local_order->add_order_note( "Product with retailer ID {$item['retailer_id']} not found" );
				continue;
			}

			$matching_wc_order_item = false;

			// check if the local order already has this item
			foreach ( $local_order->get_items() as $wc_order_item ) {

				if ( ! $wc_order_item instanceof \WC_Order_Item_Product ) {
					continue;
				}

				$order_item_product_id = $wc_order_item->get_variation_id() ?: $wc_order_item->get_product_id();

				if ( $product->get_id() === $order_item_product_id ) {
					$matching_wc_order_item = $wc_order_item;
					break;
				}
			}

			if ( empty( $matching_wc_order_item ) ) {

				$matching_wc_order_item = new \WC_Order_Item_Product();
				$matching_wc_order_item->set_product( $product );

				$local_order->add_item( $matching_wc_order_item );
			}

			$matching_wc_order_item->set_quantity( $item['quantity'] );
			$matching_wc_order_item->set_subtotal( $item['quantity'] * $item['price_per_unit']['amount'] );
			$matching_wc_order_item->set_total( $item['quantity'] * $item['price_per_unit']['amount'] );
			// we use the estimated_tax because the captured_tax represents the tax after the order/item has been shipped and we don't fulfill order at the line-item level
			$matching_wc_order_item->set_taxes(
				array(
					'subtotal' => array( $item['tax_details']['estimated_tax']['amount'] ),
					'total'    => array( $item['tax_details']['estimated_tax']['amount'] ),
				)
			);
			$matching_wc_order_item->save();

			if ( ! empty( $item['tax_details']['estimated_tax']['amount'] ) ) {

				$total_items_tax += $item['tax_details']['estimated_tax']['amount'];
			}
		}

		// update information from selected_shipping_option
		$selected_shipping_option = $remote_order->get_selected_shipping_option();

		$matching_shipping_order_item = false;

		// check if the local order already has this item
		if ( ! empty( $shipping_order_items = $local_order->get_items( 'shipping' ) ) ) {

			/** @var \WC_Order_Item_Shipping $shipping_order_item */
			foreach ( $shipping_order_items as $shipping_order_item ) {

				if ( $selected_shipping_option['name'] === $shipping_order_item->get_method_title() ) {
					$matching_shipping_order_item = $shipping_order_item;
				}
			}
		}

		if ( empty( $matching_shipping_order_item ) ) {

			$matching_shipping_order_item = new \WC_Order_Item_Shipping();
			$matching_shipping_order_item->set_method_title( $selected_shipping_option['name'] );

			$local_order->add_item( $matching_shipping_order_item );
		}

		$matching_shipping_order_item->set_total( $selected_shipping_option['price']['amount'] );
		$matching_shipping_order_item->set_taxes(
			array(
				'total' => array( $selected_shipping_option['calculated_tax']['amount'] ),
			)
		);
		$matching_shipping_order_item->save();

		// add tax item
		$matching_tax_order_item = false;

		// check if the local order already has a tax item item
		if ( ! empty( $tax_order_items = $local_order->get_items( 'tax' ) ) ) {
			$matching_tax_order_item = current( $tax_order_items );
		}

		if ( empty( $matching_tax_order_item ) ) {

			$matching_tax_order_item = new \WC_Order_Item_Tax();
			$local_order->add_item( $matching_tax_order_item );
		}

		$matching_tax_order_item->set_tax_total( $total_items_tax );
		$matching_tax_order_item->set_shipping_tax_total( $selected_shipping_option['calculated_tax']['amount'] );
		$matching_tax_order_item->save();

		$local_order->set_shipping_total( $selected_shipping_option['price']['amount'] );
		$local_order->set_shipping_tax( $selected_shipping_option['calculated_tax']['amount'] );

		// update information from shipping_address
		$shipping_address = $remote_order->get_shipping_address();

		if ( ! empty( $shipping_address['name'] ) ) {

			if ( strpos( $shipping_address['name'], ' ' ) !== false ) {

				list( $first_name, $last_name ) = explode( ' ', $shipping_address['name'], 2 );
				$local_order->set_shipping_first_name( $first_name );
				$local_order->set_shipping_last_name( $last_name );

			} else {

				$local_order->set_shipping_last_name( $shipping_address['name'] );
			}
		}

		if ( ! empty( $shipping_address['street1'] ) ) {
			$local_order->set_shipping_address_1( $shipping_address['street1'] );
		}

		if ( ! empty( $shipping_address['street2'] ) ) {
			$local_order->set_shipping_address_2( $shipping_address['street2'] );
		}

		if ( ! empty( $shipping_address['city'] ) ) {
			$local_order->set_shipping_city( $shipping_address['city'] );
		}

		if ( ! empty( $shipping_address['state'] ) ) {
			$local_order->set_shipping_state( $shipping_address['state'] );
		}

		if ( ! empty( $shipping_address['postal_code'] ) ) {
			$local_order->set_shipping_postcode( $shipping_address['postal_code'] );
		}

		if ( ! empty( $shipping_address['country'] ) ) {
			$local_order->set_shipping_country( $shipping_address['country'] );
		}

		// update information from estimated_payment_details
		$estimated_payment_details = $remote_order->get_estimated_payment_details();

		// we do not use subtotal values from the API because WC calculates them on the fly based on the items
		$local_order->set_total( $estimated_payment_details['total_amount']['amount'] );
		$local_order->set_currency( $estimated_payment_details['total_amount']['currency'] );

		// update information from buyer_details
		$buyer_details = $remote_order->get_buyer_details();

		if ( ! empty( $buyer_details ) ) {

			if ( ! empty( $buyer_details['name'] ) ) {

				if ( strpos( $buyer_details['name'], ' ' ) !== false ) {

					list( $first_name, $last_name ) = explode( ' ', $buyer_details['name'], 2 );
					$local_order->set_billing_first_name( $first_name );
					$local_order->set_billing_last_name( $last_name );

				} else {

					$local_order->set_billing_last_name( $buyer_details['name'] );
				}
			}

			if ( ! empty( $buyer_details['email'] ) ) {
				$local_order->set_billing_email( $buyer_details['email'] );
			}

			$local_order->update_meta_data( self::EMAIL_REMARKETING_META_KEY, wc_bool_to_string( $buyer_details['email_remarketing_option'] ) );
		}

		// set remote ID
		$local_order->update_meta_data( self::REMOTE_ID_META_KEY, $remote_order->get_id() );

		// always reduce stock levels
		wc_reduce_stock_levels( $local_order );

		$local_order->save();

		return $local_order;
	}


	/**
	 * Updates WooCommerce’s Orders by fetching orders from the API and either creating or updating local orders.
	 *
	 * @since 2.1.0
	 */
	public function update_local_orders() {
		// sanity check for connection status
		if ( ! facebook_for_woocommerce()->get_commerce_handler()->is_connected() ) {
			return;
		}

		$page_id = facebook_for_woocommerce()->get_integration()->get_facebook_page_id();

		try {

			$response = facebook_for_woocommerce()->get_api( facebook_for_woocommerce()->get_connection_handler()->get_page_access_token() )->get_new_orders( $page_id );

		} catch ( SV_WC_API_Exception $exception ) {

			facebook_for_woocommerce()->log( 'Error fetching Commerce orders from the Orders API: ' . $exception->getMessage() );

			return;
		}

		$remote_orders = $response->get_orders();

		foreach ( $remote_orders as $remote_order ) {

			$local_order = $this->find_local_order( $remote_order->get_id() );

			try {

				if ( empty( $local_order ) ) {
					$local_order = $this->create_local_order( $remote_order );
				} else {
					$local_order = $this->update_local_order( $remote_order, $local_order );
				}
			} catch ( \Exception $exception ) {

				if ( ! empty( $local_order ) ) {
					// add note to order
					$local_order->add_order_note( 'Error updating local order from Commerce order from the Orders API: ' . $exception->getMessage() );
				} else {
					facebook_for_woocommerce()->log( 'Error creating local order from Commerce order from the Orders API: ' . $exception->getMessage() );
				}

				continue;
			}

			if ( ! empty( $local_order ) && Order::STATUS_CREATED === $remote_order->get_status() ) {

				// acknowledge the order
				try {

					facebook_for_woocommerce()->get_api( facebook_for_woocommerce()->get_connection_handler()->get_page_access_token() )->acknowledge_order( $remote_order->get_id(), $local_order->get_id() );

					$local_order->set_status( 'processing' );

					/* translators: Placeholders: %1$s - order remote id, %2$s - order created by */
					$local_order->add_order_note(
						sprintf(
							__( 'Order %1$s paid in %2$s', 'facebook-for-woocommerce' ),
							$remote_order->get_id(),
							ucfirst( $remote_order->get_channel() )
						)
					);

				} catch ( SV_WC_API_Exception $exception ) {

					$local_order->add_order_note( 'Error acknowledging the order: ' . $exception->getMessage() );

					// if we have a clear indication that the order was not found, cancel it locally
					if ( 803 === (int) $exception->getCode() ) {
						$local_order->set_status( 'cancelled' );
					}
				}

				$local_order->save();
			}
		}

		// update any local orders that have since been cancelled on Facebook
		$this->update_cancelled_orders();
	}


	/**
	 * Updates any local orders that have since been cancelled on Facebook.
	 *
	 * @since 2.1.0
	 */
	public function update_cancelled_orders() {

		$page_id = facebook_for_woocommerce()->get_integration()->get_facebook_page_id();

		try {

			$response = facebook_for_woocommerce()->get_api( facebook_for_woocommerce()->get_connection_handler()->get_page_access_token() )->get_cancelled_orders( $page_id );

		} catch ( SV_WC_API_Exception $exception ) {

			facebook_for_woocommerce()->log( 'Error fetching Commerce orders from the Orders API: ' . $exception->getMessage() );

			return;
		}

		foreach ( $response->get_orders() as $remote_order ) {

			$local_order = $this->find_local_order( $remote_order->get_id() );

			if ( ! $local_order instanceof \WC_Order || 'cancelled' === $local_order->get_status() ) {
				continue;
			}

			$local_order->set_status( 'cancelled' );
			$local_order->save();

			wc_increase_stock_levels( $local_order );
		}
	}


	/**
	 * Frequency in seconds that orders are updated.
	 *
	 * @since 2.1.0
	 *
	 * @return int
	 */
	public function get_order_update_interval() {

		$default_interval = 5 * MINUTE_IN_SECONDS;

		/**
		 * Filters the interval between querying Facebook for new or updated orders.
		 *
		 * @since 2.1.0
		 *
		 * @param int $interval interval in seconds. Defaults to 5 minutes, and the minimum interval is 120 seconds.
		 */
		$interval = apply_filters( 'wc_facebook_commerce_order_update_interval', $default_interval );

		// if given a valid number, ensure it's 120 seconds at a minimum
		if ( is_numeric( $interval ) ) {
			$interval = max( 2 * MINUTE_IN_SECONDS, $interval );
		} else {
			$interval = $default_interval; // invalid values should get the default
		}

		return $interval;
	}


	/**
	 * Schedules a recurring ACTION_FETCH_ORDERS action, if not already scheduled.
	 *
	 * @internal
	 *
	 * @since 2.1.0
	 */
	public function schedule_local_orders_update() {
		if ( facebook_for_woocommerce()->get_commerce_handler()->is_connected() && false === as_next_scheduled_action( self::ACTION_FETCH_ORDERS, array(), \WC_Facebookcommerce::PLUGIN_ID ) ) {

			$interval = $this->get_order_update_interval();

			as_schedule_recurring_action( time() + $interval, $interval, self::ACTION_FETCH_ORDERS, array(), \WC_Facebookcommerce::PLUGIN_ID );
		}
	}


	/**
	 * Adds the necessary action & filter hooks.
	 *
	 * @since 2.1.0
	 */
	public function add_hooks() {

		// schedule a recurring ACTION_FETCH_ORDERS action, if not already scheduled
		add_action( 'init', array( $this, 'schedule_local_orders_update' ) );

		add_action( self::ACTION_FETCH_ORDERS, array( $this, 'update_local_orders' ) );

		// prevent sending emails for Commerce orders
		add_action( 'woocommerce_email_enabled_customer_completed_order', array( $this, 'maybe_stop_order_email' ), 10, 2 );
		add_action( 'woocommerce_email_enabled_customer_processing_order', array( $this, 'maybe_stop_order_email' ), 10, 2 );
		add_action( 'woocommerce_email_enabled_customer_refunded_order', array( $this, 'maybe_stop_order_email' ), 10, 2 );
		add_action( 'woocommerce_email_enabled_customer_partially_refunded_order', array( $this, 'maybe_stop_order_email' ), 10, 2 );
	}


	/**
	 * Fulfills an order via API.
	 *
	 * In addition to the exceptions we throw for missing data, the API request will also fail if:
	 * - The stored remote ID is invalid
	 * - The order has an item with a retailer ID that was not originally part of the order
	 * - An item has a different quantity than what was originally ordered
	 * - The remote order was already fulfilled
	 *
	 * @since 2.1.0
	 *
	 * @param \WC_Order $order order object
	 * @param string    $tracking_number shipping tracking number
	 * @param string    $carrier shipping carrier
	 * @throws SV_WC_Plugin_Exception
	 */
	public function fulfill_order( \WC_Order $order, $tracking_number, $carrier ) {

		try {

			$remote_id = $order->get_meta( self::REMOTE_ID_META_KEY );

			if ( ! $remote_id ) {
				throw new SV_WC_Plugin_Exception( __( 'Remote ID not found.', 'facebook-for-woocommerce' ) );
			}

			$shipment_utilities = new Utilities\Shipment();

			if ( ! $shipment_utilities->is_valid_carrier( $carrier ) ) {
				/** translators: Placeholders: %s - shipping carrier code */
				throw new SV_WC_Plugin_Exception( sprintf( __( '%s is not a valid shipping carrier code.', 'facebook-for-woocommerce' ), $carrier ) );
			}

			$items = array();

			/** @var \WC_Order_Item_Product $item */
			foreach ( $order->get_items() as $item ) {

				if ( $product = $item->get_product() ) {

					$items[] = array(
						'retailer_id' => \WC_Facebookcommerce_Utils::get_fb_retailer_id( $product ),
						'quantity'    => $item->get_quantity(),
					);
				}
			}

			if ( empty( $items ) ) {
				throw new SV_WC_Plugin_Exception( __( 'No valid Facebook products were found.', 'facebook-for-woocommerce' ) );
			}

			$fulfillment_data = array(
				'items'         => $items,
				'tracking_info' => array(
					'carrier'         => $carrier,
					'tracking_number' => $tracking_number,
				),
			);

			$plugin = facebook_for_woocommerce();

			$plugin->get_api( $plugin->get_connection_handler()->get_page_access_token() )->fulfill_order( $remote_id, $fulfillment_data );

			$order->add_order_note(
				sprintf(
				/* translators: Placeholder: %s - sales channel name, like Facebook or Instagram */
					__( '%s order fulfilled.', 'facebook-for-woocommerce' ),
					ucfirst( $order->get_created_via() )
				)
			);

		} catch ( SV_WC_Plugin_Exception $exception ) {

			$order->add_order_note(
				sprintf(
				/* translators: Placeholders: %1$s - sales channel name, like Facebook or Instagram, %2$s - error message */
					__( '%1$s order could not be fulfilled. %2$s', 'facebook-for-woocommerce' ),
					ucfirst( $order->get_created_via() ),
					$exception->getMessage()
				)
			);

			throw $exception;
		}
	}


	/**
	 * Refunds an order.
	 *
	 * @since 2.1.0
	 *
	 * @param \WC_Order_Refund $refund order refund object
	 * @param string           $reason_code refund reason code
	 * @throws SV_WC_Plugin_Exception
	 */
	public function add_order_refund( \WC_Order_Refund $refund, $reason_code ) {

		$plugin = facebook_for_woocommerce();

		$api = $plugin->get_api( $plugin->get_connection_handler()->get_page_access_token() );

		$valid_reason_codes = array(
			self::REFUND_REASON_BUYERS_REMORSE,
			self::REFUND_REASON_DAMAGED_GOODS,
			self::REFUND_REASON_NOT_AS_DESCRIBED,
			self::REFUND_REASON_QUALITY_ISSUE,
			self::REFUND_REASON_OTHER,
			self::REFUND_REASON_WRONG_ITEM,
		);

		if ( ! in_array( $reason_code, $valid_reason_codes, true ) ) {
			$reason_code = self::REFUND_REASON_OTHER;
		}

		try {

			$parent_order = wc_get_order( $refund->get_parent_id() );

			if ( ! $parent_order instanceof \WC_Order ) {
				throw new SV_WC_Plugin_Exception( __( 'Parent order not found.', 'facebook-for-woocommerce' ) );
			}

			$remote_id = $parent_order->get_meta( self::REMOTE_ID_META_KEY );

			if ( ! $remote_id ) {
				throw new SV_WC_Plugin_Exception( __( 'Remote ID for parent order not found.', 'facebook-for-woocommerce' ) );
			}

			$refund_data = array(
				'reason_code' => $reason_code,
			);

			if ( ! empty( $reason_text = $refund->get_reason() ) ) {
				$refund_data['reason_text'] = $reason_text;
			}

			// only send items for partial refunds
			if ( $parent_order->get_total() - $refund->get_amount() > 0 ) {
				$refund_data['items'] = $this->get_refund_items( $refund );
			}

			if ( ! empty( $refund->get_shipping_total() ) ) {

				$refund_data['shipping'] = array(
					'shipping_refund' => array(
						'amount'   => abs( $refund->get_shipping_total() ),
						'currency' => $refund->get_currency(),
					),
				);
			}

			$api->add_order_refund( $remote_id, $refund_data );

			$parent_order->add_order_note(
				sprintf(
				/* translators: Placeholder: %s - sales channel name, like Facebook or Instagram */
					__( 'Order refunded on %s.', 'facebook-for-woocommerce' ),
					ucfirst( $parent_order->get_created_via() )
				)
			);

		} catch ( SV_WC_Plugin_Exception $exception ) {

			if ( ! empty( $parent_order ) && $parent_order instanceof \WC_Order ) {

				$parent_order->add_order_note(
					sprintf(
					/* translators: Placeholders: %1$s - sales channel name, like Facebook or Instagram, %2$s - error message */
						__( 'Could not refund %1$s order: %2$s', 'facebook-for-woocommerce' ),
						ucfirst( $parent_order->get_created_via() ),
						$exception->getMessage()
					)
				);

			} else {

				facebook_for_woocommerce()->log( "Could not refund remote order for order refund {$refund->get_id()}: {$exception->getMessage()}" );
			}

			// re-throw the exception so the error halts refund creation
			throw $exception;
		}
	}


	/**
	 * Gets the Facebook items from the given refund.
	 *
	 * @since 2.1.0
	 *
	 * @param \WC_Order_Refund $refund refund object
	 * @return array
	 * @throws SV_WC_Plugin_Exception
	 */
	private function get_refund_items( \WC_Order_Refund $refund ) {

		$items = array();

		/** @var \WC_Order_Item_Product $item */
		foreach ( $refund->get_items() as $item ) {

			if ( $product = $item->get_product() ) {

				$refund_item = array(
					'retailer_id' => \WC_Facebookcommerce_Utils::get_fb_retailer_id( $product ),
				);

				if ( ! empty( $item->get_quantity() ) ) {

					$refund_item['item_refund_quantity'] = abs( $item->get_quantity() );

				} else {

					$refund_item['item_refund_amount'] = array(
						'amount'   => abs( $item->get_total() ),
						'currency' => $refund->get_currency(),
					);
				}

				$items[] = $refund_item;
			}
		}

		if ( empty( $items ) ) {
			throw new SV_WC_Plugin_Exception( __( 'No valid Facebook products were found.', 'facebook-for-woocommerce' ) );
		}

		return $items;
	}


	/**
	 * Cancels an order.
	 *
	 * @since 2.1.0
	 *
	 * @param \WC_Order $order order object
	 * @param string    $reason_code cancellation reason code
	 * @throws SV_WC_Plugin_Exception
	 */
	public function cancel_order( \WC_Order $order, $reason_code ) {

		$plugin = facebook_for_woocommerce();

		$api = $plugin->get_api( $plugin->get_connection_handler()->get_page_access_token() );

		$valid_reason_codes = array_keys( $this->get_cancellation_reasons() );

		if ( ! in_array( $reason_code, $valid_reason_codes, true ) ) {
			$reason_code = self::CANCEL_REASON_OTHER;
		}

		try {

			$remote_id = $order->get_meta( self::REMOTE_ID_META_KEY );

			if ( ! $remote_id ) {
				throw new SV_WC_Plugin_Exception( __( 'Remote ID not found.', 'facebook-for-woocommerce' ) );
			}

			$api->cancel_order( $remote_id, $reason_code );

			$order->add_order_note(
				sprintf(
				/* translators: Placeholder: %s - sales channel name, like Facebook or Instagram */
					__( '%s order cancelled.', 'facebook-for-woocommerce' ),
					ucfirst( $order->get_created_via() )
				)
			);

		} catch ( SV_WC_Plugin_Exception $exception ) {

			$order->add_order_note(
				sprintf(
				/* translators: Placeholders: %1$s - sales channel name, like Facebook or Instagram, %2$s - error message */
					__( '%1$s order could not be cancelled. %2$s', 'facebook-for-woocommerce' ),
					ucfirst( $order->get_created_via() ),
					$exception->getMessage()
				)
			);

			throw $exception;
		}
	}


	/**
	 * Gets the valid cancellation reasons.
	 *
	 * @since 2.1.0
	 *
	 * @return array key-value array with codes and their labels
	 */
	public function get_cancellation_reasons() {

		return array(

			self::CANCEL_REASON_CUSTOMER_REQUESTED => __( 'Customer requested cancellation', 'facebook-for-woocommerce' ),
			self::CANCEL_REASON_OUT_OF_STOCK       => __( 'Product(s) are out of stock', 'facebook-for-woocommerce' ),
			self::CANCEL_REASON_INVALID_ADDRESS    => __( 'Customer address is invalid', 'facebook-for-woocommerce' ),
			self::CANCEL_REASON_SUSPICIOUS_ORDER   => __( 'Suspicious order', 'facebook-for-woocommerce' ),
			self::CANCEL_REASON_OTHER              => __( 'Other', 'facebook-for-woocommerce' ),
		);
	}


	/**
	 * Prevents sending emails for Commerce orders.
	 *
	 * @internal
	 *
	 * @since 2.1.0
	 *
	 * @param bool      $is_enabled whether the email is enabled in the first place
	 * @param \WC_Order $order order object
	 * @return bool
	 */
	public function maybe_stop_order_email( $is_enabled, $order ) {

		// will decide whether to allow $is_enabled to be filtered
		$is_previously_enabled = $is_enabled;

		// checks whether or not the order is a Commerce order
		$is_commerce_order = $order instanceof \WC_Order && self::is_commerce_order( $order );

		// decides whether to disable or to keep emails enabled
		$is_enabled = $is_enabled && ! $is_commerce_order;

		if ( $is_previously_enabled && $is_commerce_order ) {

			/**
			 * Filters the flag used to determine whether the email is enabled.
			 *
			 * @since 2.1.0
			 *
			 * @param bool $is_enabled whether the email is enabled
			 * @param \WC_Order $order order object
			 * @param Orders $this admin orders instance
			 */
			$is_enabled = (bool) apply_filters( 'wc_facebook_commerce_send_woocommerce_emails', $is_enabled, $order, $this );
		}

		return $is_enabled;
	}


}