<?php

if (
	class_exists( 'Tribe__Tickets_Plus__Commerce__WooCommerce__Main' )
	|| ! class_exists( 'Tribe__Tickets__Tickets' )
) {
	return;
}

use Tribe__Utils__Array as Arr;
use Automattic\WooCommerce\Utilities\OrderUtil;
use Automattic\WooCommerce\Utilities\FeaturesUtil;

class Tribe__Tickets_Plus__Commerce__WooCommerce__Main extends Tribe__Tickets_Plus__Tickets {

	/**
	 * {@inheritdoc}
	 */
	public $orm_provider = 'woo';

	/**
	 * Name of the CPT that holds Attendees (tickets holders).
	 *
	 * @since 6.4.0
	 *
	 * @var string
	 */
	const ATTENDEE_OBJECT = 'tribe_wooticket';

	/**
	 * Name of the CPT that holds Attendees (tickets holders).
	 *
	 * @deprecated 6.4.0 Use ATTENDEE_OBJECT const instead.
	 *
	 * @var string
	 */
	public $attendee_object = 'tribe_wooticket';

	/**
	 * Meta key used to track the total number of WooCommerce tickets sold for each event.
	 *
	 * @since 6.7.0
	 * @var string
	 */
	const TOTAL_SALES = 'total_sales';

	/**
	 * Prefix used to generate the key of the transient to be associated with a different user session to avoid
	 * redirection for users with no transient present.
	 *
	 * @since 4.7.3
	 *
	 * @var string
	 */
	private $cart_location_cache_prefix = 'tribe_woo_cart_hash_';

	/**
	 * Name of the CPT that holds Orders when HPOS is enabled.
	 *
	 * @deprecated 4.7 Use $order_object variable instead
	 */
	const ORDER_OBJECT = 'shop_order';

	/**
	 * Name of the CPT that holds Orders when HPOS is disabled.
	 *
	 * @var string
	 */
	public $order_object = 'shop_order';

	/**
	 * Name of the CPT that holds Orders when HPOS is enabled.
	 *
	 * @since 5.9.1
	 *
	 * @var string
	 */
	protected string $order_object_hpos = 'shop_order_placehold';

	/**
	 * Plugin name.
	 *
	 * @since 5.10.3
	 *
	 * @var string
	 */
	public $plugin_name;

	/**
	 * Plugin slug.
	 *
	 * @since 5.10.3
	 *
	 * @var string
	 */
	protected $plugin_slug;

	/**
	 * Plugin slug.
	 *
	 * @since 5.10.3
	 *
	 * @var string
	 */
	protected $pluginSlug; // phpcs:ignore WordPress.NamingConventions.ValidVariableName.PropertyNotSnakeCase
	/**
	 * Plugin path.
	 *
	 * @since 5.10.3
	 *
	 * @var string
	 */
	protected $plugin_path;

	/**
	 * Plugin directory.
	 *
	 * @since 5.10.3
	 *
	 * @var string
	 */
	protected $plugin_dir;

	/**
	 * Plugin directory.
	 *
	 * @since 5.10.3
	 *
	 * @var string
	 */
	protected $pluginDir; // phpcs:ignore WordPress.NamingConventions.ValidVariableName.PropertyNotSnakeCase

	/**
	 * Plugin URL.
	 *
	 * @since 5.10.3
	 *
	 * @var string
	 */
	protected $plugin_url;

	/**
	 * Meta key that relates Attendees and Products.
	 *
	 * @deprecated 4.7 Use $attendee_product_key variable instead
	 */
	const ATTENDEE_PRODUCT_KEY = '_tribe_wooticket_product';

	/**
	 * Meta key that relates Attendees and Products.
	 *
	 * @var string
	 */
	public $attendee_product_key = '_tribe_wooticket_product';

	/**
	 * Meta key that relates Attendees and Orders.
	 *
	 * @since 6.4.0
	 *
	 * @var string
	 */
	const ATTENDEE_ORDER_KEY = '_tribe_wooticket_order';

	/**
	 * Meta key that relates Attendees and Orders.
	 *
	 * @deprecated 6.4.0 Use ATTENDEE_ORDER_KEY const instead.
	 *
	 * @var string
	 */
	public $attendee_order_key = '_tribe_wooticket_order';

	/**
	 * Meta key that relates Attendees and Order Items.
	 *
	 * @since 4.3.2
	 * @var   string
	 */
	public $attendee_order_item_key = '_tribe_wooticket_order_item';

	/**
	 * Meta key that relates Attendees and Events.
	 *
	 * @since 6.4.0
	 *
	 * @var string
	 */
	const ATTENDEE_EVENT_KEY = '_tribe_wooticket_event';

	/**
	 * Meta key that relates Attendees and Events.
	 *
	 * @deprecated 6.4.0 Use ATTENDEE_EVENT_KEY const instead
	 *
	 * @var string
	 */
	public $attendee_event_key = '_tribe_wooticket_event';

	/**
	 * Meta key that relates Products and Events
	 *
	 * @var string
	 */
	public $event_key = '_tribe_wooticket_for_event';

	/**
	 * Meta key that stores if an attendee has checked in to an event
	 *
	 * @var string
	 */
	public $checkin_key = '_tribe_wooticket_checkedin';

	/**
	 * Meta key that holds the security code that's printed in the tickets
	 *
	 * @var string
	 */
	public $security_code = '_tribe_wooticket_security_code';

	/**
	 * Meta key that holds if an order has tickets (for performance)
	 *
	 * @var string
	 */
	public $order_has_tickets = '_tribe_has_tickets';

	/**
	 * Meta key that will keep track of whether the confirmation mail for a ticket has been sent to the user or not.
	 *
	 * @var string
	 */
	public $mail_sent_meta_key = '_tribe_mail_sent';

	/**
	 * Meta key that holds the name of a ticket to be used in reports if the Product is deleted
	 *
	 * @var string
	 */
	public $deleted_product = '_tribe_deleted_product_name';

	/**
	 * Name of the ticket commerce CPT.
	 *
	 * @var string
	 */
	public $ticket_object = 'product';

	/**
	 * Meta key that holds if the attendee has opted out of the front-end listing
	 *
	 * @deprecated 4.7 Use static $attendee_optout_key variable instead
	 *
	 * @var string
	 */
	const ATTENDEE_OPTOUT_KEY = '_tribe_wooticket_attendee_optout';

	/**
	 * Meta key that holds if the attendee has opted out of the front-end listing
	 *
	 * @var string
	 */
	public $attendee_optout_key = '_tribe_wooticket_attendee_optout';

	/**
	 * Meta key that holds the full name of the ticket attendee.
	 *
	 * @since 5.1.0
	 *
	 * @var string
	 */
	public $full_name = '_tribe_tickets_full_name';

	/**
	 * Meta key that holds the email of the ticket attendee.
	 *
	 * @since 5.1.0
	 *
	 * @var string
	 */
	public $email = '_tribe_tickets_email';

	/**
	 * Meta key that holds if the order was restocked already or not.
	 *
	 * @since 5.2.5
	 *
	 * @var string
	 */
	public $restocked_refunded_order = '_tribe_tickets_restocked_refunded_order';

	/**
	 * Order count cache key.
	 *
	 * @since 4.11.0.2
	 *
	 * @var string
	 */
	const ORDER_COUNT_CACHE_KEY = '_tribe_woo_order_item_count';

	/**
	 * If we've displayed the cart notice yet.
	 *
	 * @since 4.11.0
	 *
	 * @var boolean
	 */
	public $cart_change_notice_displayed = false;

	/**
	 * @var WC_Product|WC_Product_Simple
	 */
	protected $product;

	/**
	 * Holds an instance of the Tribe__Tickets_Plus__Commerce__WooCommerce__Email class
	 *
	 * @var Tribe__Tickets_Plus__Commerce__WooCommerce__Email
	 */
	private $mailer = null;

	/** @var Tribe__Tickets_Plus__Commerce__WooCommerce__Settings */
	private $settings;

	/**
	 * Instance of this class for use as singleton
	 */
	private static $instance;

	/**
	 * Instance of Tribe__Tickets_Plus__Commerce__WooCommerce__Meta
	 */
	private static $meta;

	/**
	 * @var Tribe__Tickets_Plus__Commerce__WooCommerce__Global_Stock
	 */
	private static $global_stock;

	/**
	 * For each ticket, stores the total number of pending orders.
	 *
	 * Populates lazily and on-demand.
	 *
	 * @since 4.4.9
	 *
	 * @var array
	 */
	protected $pending_orders_by_ticket = [];

	/**
	 * Current version of this plugin
	 */
	const VERSION = '4.5.0.1';

	/**
	 * Min required The Events Calendar version
	 */
	const REQUIRED_TEC_VERSION = '4.6.20';

	/**
	 * Min required WooCommerce version
	 */
	const REQUIRED_WC_VERSION = '3.7';

	/**
	 * Creates the instance of the class
	 *
	 * @static
	 * @return void
	 */
	public static function init() {
		self::$instance = self::get_instance();
	}

	/**
	 * Get (and instantiate, if necessary) the instance of the class
	 *
	 * @static
	 * @return Tribe__Tickets_Plus__Commerce__WooCommerce__Main
	 */
	public static function get_instance() {
		return tribe( 'tickets-plus.commerce.woo' );
	}

	/**
	 * Sets the compatibility checks for WooCommerce.
	 *
	 * Used by the WooCommerce Commerce Module to configure compatibility with certain features of WooCommerce.
	 *
	 * The action is set to priority 100 to ensure this code runs as late as possible,
	 * guaranteeing that WooCommerce's functionality is fully loaded before proceeding.
	 *
	 * By passing true to class_exists(), we allow the autoloader to attempt loading the class
	 * if it's not already loaded. WooCommerce autoloads most of its classes early in the
	 * initialization process, so there is minimal risk that the class isn't available.
	 * This also ensures that we don’t miss the class if it's available but hasn't been explicitly loaded yet.
	 *
	 * The hooks here are intentionally using anonymous methods as we do not want them to be removed.
	 *
	 * @since 5.8.0
	 * @since 5.9.1 Changed compatibility to true.
	 * @since 6.0.3 Updated compatibility logic for WooCommerce HPOS.
	 *
	 * @return void
	 */
	public static function set_compatibility_checks(): void {
		add_action(
			'before_woocommerce_init',
			static function () {
				if ( ! class_exists( FeaturesUtil::class, true ) ) {
					// Debugging message or further handling if necessary.
					return;
				}
				// Declare compatibility with WooCommerce's custom order tables feature.
				FeaturesUtil::declare_compatibility( 'custom_order_tables', EVENT_TICKETS_PLUS_FILE, true );
			},
			100
		);
	}

	/**
	 * Class constructor
	 */
	public function __construct() {
		/* Set up parent vars */
		$this->plugin_name = $this->pluginName = _x( 'WooCommerce', 'ticket provider', 'event-tickets-plus' );
		$this->plugin_slug = $this->pluginSlug = 'wootickets';
		$this->plugin_path = $this->pluginPath = trailingslashit( EVENT_TICKETS_PLUS_DIR );
		$this->plugin_dir  = $this->pluginDir = trailingslashit( basename( $this->plugin_path ) );
		$this->plugin_url  = $this->pluginUrl = trailingslashit( plugins_url( $this->plugin_dir ) );

		parent::__construct();

		$this->bind_implementations();
		$this->hooks();
		$this->orders_report();
		$this->global_stock();
		$this->meta();
		$this->settings();
	}

	/**
	 * Binds implementations that are specific to WooCommerce
	 *
	 * @see \Tribe__Tickets_Plus__Commerce__Woocommerce__Cart
	 * @see \Tribe\Tickets\Plus\Commerce\WooCommerce\Regenerate_Order_Attendees
	 */
	public function bind_implementations() {
		tribe_singleton( 'tickets-plus.commerce.woo.cart', 'Tribe__Tickets_Plus__Commerce__WooCommerce__Cart', [ 'hook' ] );
		tribe( 'tickets-plus.commerce.woo.cart' );

		tribe_singleton( 'tickets-plus.commerce.woo.regenerate-order-attendees', \Tribe\Tickets\Plus\Commerce\WooCommerce\Regenerate_Order_Attendees::class, [ 'hook' ] );
		tribe( 'tickets-plus.commerce.woo.regenerate-order-attendees' );
	}

	/**
	 * Registers all actions/filters
	 *
	 * @since 6.0.5 Added filters for All Tickets page.
	 */
	public function hooks() {
		if ( ! tribe_tickets_is_woocommerce_active() ) {
			return;
		}

		add_action( 'init', [ $this, 'register_wootickets_type' ] );
		add_action( 'init', [ $this, 'register_resources' ] );
		add_action( 'add_meta_boxes', [ $this, 'woocommerce_meta_box' ] );
		add_action( 'before_delete_post', [ $this, 'handle_delete_post' ] );
		add_action( 'woocommerce_checkout_update_order_meta', [ $this, 'delayed_ticket_generation' ], 8, 1 );
		add_action( 'woocommerce_order_status_changed', [ $this, 'delayed_ticket_generation' ], 12, 3 );
		add_action( 'tribe_wc_delayed_ticket_generation', [ $this, 'generate_tickets' ] );
		add_action( 'woocommerce_order_status_changed', [ $this, 'reset_attendees_cache' ] );
		add_action( 'woocommerce_email_header', [ $this, 'maybe_add_tickets_msg_to_email' ], 10, 2 );
		add_action( 'tribe_events_tickets_metabox_edit_advanced', [ $this, 'do_metabox_advanced_options' ], 10, 2 );

		if ( class_exists( 'Tribe__Events__API' ) ) {
			add_action( 'woocommerce_product_quick_edit_save', [ $this, 'syncronize_product_editor_changes' ] );
			add_action( 'woocommerce_process_product_meta_simple', [ $this, 'syncronize_product_editor_changes' ] );
		}

		add_filter( 'tribe_tickets_get_modules', [ $this, 'maybe_remove_as_active_module' ] );

		if (
			function_exists( 'WC' ) // In case this gets called when WooCommerce is deactivated.
			&& version_compare( WC()->version, '3.0', '<' ) // Pre-3.0 installs.
		) {
			add_action( 'woocommerce_add_order_item_meta', [ $this, 'set_attendee_optout_choice' ], 15, 2 );
		} else {
			add_action( 'woocommerce_checkout_create_order_line_item', [ $this, 'set_attendee_optout_value' ], 10, 3 );
		}

		add_action( 'woocommerce_checkout_order_review', [ $this, 'add_checkout_links' ], 5 );

		// Enqueue styles
		add_action( 'wp_enqueue_scripts', [ $this, 'enqueue_styles' ], 11 );
		add_action( 'admin_enqueue_scripts', [ $this, 'admin_enqueue_styles' ], 11 );

		add_filter( 'post_type_link', [ $this, 'hijack_ticket_link' ], 10, 4 );
		add_filter( 'woocommerce_email_classes', [ $this, 'add_email_class_to_woocommerce' ] );

		add_action( 'woocommerce_resend_order_emails_available', [ $this, 'add_resend_tickets_action' ] ); // WC 3.1.x
		add_action( 'woocommerce_order_actions', [ $this, 'add_resend_tickets_action' ] ); // WC 3.2.x
		add_action( 'woocommerce_order_action_resend_tickets_email', [ $this, 'send_tickets_email' ] ); // WC 3.2.x

		add_filter( 'woocommerce_order_actions', [ $this, 'add_restock_action_for_refunded_order' ] );
		add_action( 'woocommerce_order_action_event_tickets_plus_restock_refunded_tickets', [ $this, 'handle_restock_action_for_refunded_order' ] );

		add_filter( 'event_tickets_attendees_woo_checkin_stati', tribe_callback( 'tickets-plus.commerce.woo.checkin-stati', 'filter_attendee_ticket_checkin_stati' ), 10 );
		add_action( 'wootickets_checkin', [ $this, 'purge_attendees_transient' ] );
		add_action( 'wootickets_uncheckin', [ $this, 'purge_attendees_transient' ] );
		add_filter( 'tribe_tickets_settings_post_types', [ $this, 'exclude_product_post_type' ] );

		add_action( 'tribe_tickets_attendees_page_inside', [ $this, 'render_tabbed_view' ] );
		add_action( 'woocommerce_check_cart_items', [ $this, 'validate_tickets' ] );
		add_action( 'template_redirect', [ $this, 'redirect_to_cart' ] );

		add_action( 'wc_after_products_starting_sales', [ $this, 'syncronize_products' ] );
		add_action( 'wc_after_products_ending_sales', [ $this, 'syncronize_products' ] );

		add_action( 'tribe_ticket_available_warnings', [ $this, 'get_ticket_table_warnings' ], 10, 2 );

		add_filter( 'tribe_tickets_get_ticket_max_purchase', [ $this, 'filter_ticket_max_purchase' ], 10, 2 );

		tribe_singleton( 'commerce.woo.order.refunded', 'Tribe__Tickets_Plus__Commerce__WooCommerce__Orders__Refunded' );

		add_filter( 'tribe_attendee_registration_form_classes', [ $this, 'tribe_attendee_registration_form_class' ] );
		add_filter( 'tribe_attendee_registration_cart_provider', [ $this, 'tribe_attendee_registration_cart_provider' ], 10, 2 );
		add_action( 'tribe_tickets_registration_content_before_all_events', [ $this, 'woo_attendee_registration_notice_cart_qty_change' ], 10, 3 );
		add_action( 'woocommerce_after_checkout_form', [ $this, 'clear_tribe_ar_ticket_updated' ] );
		add_filter( 'tribe_tickets_plus_meta_cookie_flag', [ $this, 'clear_tribe_ar_ticket_updated' ] );

		add_filter( 'tribe_tickets_cart_urls', [ $this, 'add_cart_url' ] );
		add_filter( 'tribe_tickets_checkout_urls', [ $this, 'add_checkout_url' ] );

		// Temporary workaround for empty orders. Remove this after WooCommerce 5.1 is released which contains a fix for this.
		add_filter( 'woocommerce_order_query', [ $this, 'temporarily_filter_order_query' ] );

		// Stock actions.
		add_action( 'event_tickets_attendee_ticket_deleted', [ $this, 'update_order_note_for_deleted_attendee' ], 10, 2 );
		add_action( 'tribe_tickets_ticket_moved', [ $this, 'create_order_for_moved_ticket' ], 10, 6 );
		add_action( 'woocommerce_before_product_object_save', [ $this, 'sync_wc_product_stock_with_ticket' ], 10, 2 );
		add_action( 'woocommerce_updated_product_stock', [ $this, 'sync_wc_product_stock_update' ] );

		// Cache invalidation.
		add_filter( 'tec_cache_listener_save_post_types', [ $this, 'filter_cache_listener_save_post_types' ] );

		add_action( 'woocommerce_after_order_details', [ $this, 'include_your_tickets' ] );

		add_filter( 'tec_tickets_get_field_meta_for_attendee', [ $this, 'get_proper_attendee_order_meta_data' ], 10, 4 );

		add_filter( 'tec_tickets_admin_tickets_table_column_sold', [ $this, 'filter_admin_tickets_table_column_sold' ], 10, 2 );
		add_filter( 'tec_tickets_admin_tickets_table_column_sales', [ $this, 'filter_admin_tickets_table_column_sales' ], 10, 2 );

		add_filter( 'tec_tickets_pre_build_ticket_properties', [ $this, 'filter_pre_build_ticket_properties' ], 10, 2 );
	}

	/**
	 * Sync direct WooCommerce stock updates that are tied with Order status updates.
	 *
	 * @since 5.6.5
	 *
	 * @param int $product_id The Product ID.
	 *
	 * @return void
	 */
	public function sync_wc_product_stock_update( $product_id ) {
		$event = $this->get_event_for_ticket( $product_id );
		if ( ! $event ) {
			return;
		}

		$product   = wc_get_product( $product_id );
		$new_stock = $product->get_stock_quantity();
		$ticket    = $this->get_ticket( $event->ID, $product->get_id() );

		// If the new stock went over the capacity then we should set it to max capacity.
		if ( $new_stock > $ticket->capacity ) {
			$product->set_stock_quantity( $ticket->capacity );
			$product->save();
		}
	}

	/**
	 * Sync Woo Product stock data to Ticket stock data.
	 *
	 * @since 5.6.5
	 *
	 * @param WC_Product    $product The object being saved.
	 * @param WC_Data_Store $data_store      THe data store persisting the data.
	 *
	 * @return void
	 */
	public function sync_wc_product_stock_with_ticket( $product, $data_store ) {
		$event = $this->get_event_for_ticket( $product->get_id() );

		if ( ! $event ) {
			return;
		}

		$changes = $product->get_changes();

		if ( empty( $changes ) || ! isset( $changes['stock_quantity'] ) ) {
			return;
		}

		$new_stock = $changes['stock_quantity'];
		$ticket = $this->get_ticket( $event->ID, $product->get_id() );

		if ( $new_stock > $ticket->capacity ) {
			$product->set_stock_quantity( $ticket->stock() );
		}
	}

	/**
	 * Create a duplicate order with the attendee data to show order report.
	 *
	 * @since 6.0.3 Fixed an issue where the data in the WooCommerce email were sent prematurely before the data was updated.
	 *
	 * @since 5.5.2 Fixed sync of WooCommerce product stock with ticket stock.
	 *
	 * @param int $attendee_id              the ticket which has been moved.
	 * @param int $src_ticket_type_id       the ticket type it belonged to originally.
	 * @param int $tgt_ticket_type_id       the ticket type it now belongs to.
	 * @param int $src_event_id             the event/post which the ticket originally belonged to.
	 * @param int $tgt_event_id             the event/post which the ticket now belongs to.
	 * @param int $instigator_id            the user who initiated the change.
	 */
	public function create_order_for_moved_ticket( $attendee_id, $src_ticket_type_id, $tgt_ticket_type_id, $src_event_id, $tgt_event_id, $instigator_id ) {
		$from_product = wc_get_product( $src_ticket_type_id );
		$to_product   = wc_get_product( $tgt_ticket_type_id );

		if ( ! $from_product || ! $to_product ) {
			return;
		}

		$attendee = $this->get_attendee( $attendee_id );
		$order    = wc_get_order( Tribe__Utils__Array::get( $attendee, 'order_id' ) );

		if ( ! $order ) {
			return;
		}

		// Temporarily disable the "New Order" email.
		add_filter( 'woocommerce_email_enabled_new_order', '__return_false' );

		// Create the new order without setting the status.
		$new_order = wc_create_order(
			[
				'parent'        => $order->get_id(),
				'customer_id'   => $order->get_customer_id(),
				'customer_note' => $order->get_customer_note(),
				'created_via'   => 'admin',
			]
		);

		// Update the product_id value to our new ticket (product).
		update_post_meta( $attendee_id, $this->attendee_product_key, $to_product->get_id() );
		// Update the order_id value to our new order.
		update_post_meta( $attendee_id, self::ATTENDEE_ORDER_KEY, $new_order->get_id() );

		// Add the product to the new order and set necessary metadata.
		$new_order->add_product( $to_product );
		$new_order->set_address( $order->get_address() );
		$new_order->set_total( 0 );

		$new_order->add_meta_data( $this->order_has_tickets, true );
		$new_order->add_meta_data( Tribe__Tickets_Plus__Meta::META_KEY, $order->get_meta( Tribe__Tickets_Plus__Meta::META_KEY ) );
		$new_order->add_order_note(
			sprintf(
			// Translators: %1$s: The Attendee Name. %2$s: Parent order edit URL. %3$s: Parent order ID.
				__( '<b> Attendee Moved : </b> <br> <i>%1$s</i> was moved into this order from <a href="%2$s">Order %3$s</a>', 'event-tickets-plus' ),
				$attendee['holder_name'],
				$order->get_edit_order_url(),
				$order->get_id()
			)
		);

		// Now set the status to match the original order and trigger the email.
		$new_order->set_status( $order->get_status() );

		// Re-enable the "New Order" email.
		remove_filter( 'woocommerce_email_enabled_new_order', '__return_false' );

		$new_order->save();

		$order->add_order_note(
			sprintf(
			// Translators: %1$s: The Attendee Name. %2$s: Copied order edit URL. %3$s: Copied order ID.
				__( '<b> Attendee Moved : </b> <br> <i>%1$s</i> is moved from this order to <a href="%2$s">Order %3$s</a>', 'event-tickets-plus' ),
				$attendee['holder_name'],
				$new_order->get_edit_order_url(),
				$new_order->get_id()
			)
		);

		$order->save();

		// Sync Product stock with ticket inventory.
		$from_ticket = $this->get_ticket( $src_event_id, $src_ticket_type_id );
		$to_ticket   = $this->get_ticket( $tgt_event_id, $tgt_ticket_type_id );

		$from_product->set_stock_quantity( $from_ticket->inventory() );
		$from_product->save();

		$to_product->set_stock_quantity( $to_ticket->inventory() );
		$to_product->save();

		// Reset caches.
		$this->reset_attendees_cache( $order->get_id() );
		$this->reset_attendees_cache( $new_order->get_id() );

		/** @var Tribe__Tickets__Tickets_Handler $handler */
		$handler = tribe( 'tickets.handler' );
		$handler->sync_shared_capacity( $src_event_id, tribe_tickets_get_capacity( $src_event_id ) );
		$handler->sync_shared_capacity( $tgt_event_id, tribe_tickets_get_capacity( $tgt_event_id ) );
	}


	public function register_resources() {
		$stylesheet_url = $this->plugin_url . 'src/resources/css/wootickets.css';

		// Get minified CSS if it exists
		$stylesheet_url = Tribe__Assets::maybe_get_min_file( $stylesheet_url, true );

		// apply filters
		$stylesheet_url = apply_filters( 'tribe_wootickets_stylesheet_url', $stylesheet_url );

		wp_register_style( 'TribeEventsWooTickets', $stylesheet_url, [], apply_filters( 'tribe_events_wootickets_css_version', self::VERSION ) );

		//Check for override stylesheet
		$user_stylesheet_url = Tribe__Tickets__Templates::locate_stylesheet( 'tribe-events/wootickets/wootickets.css' );
		$user_stylesheet_url = apply_filters( 'tribe_events_wootickets_stylesheet_url', $user_stylesheet_url );

		//If override stylesheet exists, then enqueue it
		if ( $user_stylesheet_url ) {
			wp_register_style( 'tribe-events-wootickets-override-style', $user_stylesheet_url );
		}
	}

	/**
	 * If WooCommerce is not active, remove WooTickets as an active provider.
	 *
	 * Protects against running code for past WooTicket attendees and tickets after WooCommerce is deactivated.
	 *
	 * @see   \Tribe__Tickets__Tickets::modules()
	 *
	 * @since 4.12.0
	 *
	 * @param array $active_modules The array of active provider modules.
	 *
	 * @return array
	 */
	public function maybe_remove_as_active_module( $active_modules ) {
		if ( ! function_exists( 'WC' ) ) {
			unset( $active_modules[ $this->class_name ] );
		}

		return $active_modules;
	}

	/**
	 * After placing the Order make sure we store the users option to show the Attendee Optout.
	 *
	 * This method should only be used if a version of WooCommerce lower than 3.0 is in use.
	 *
	 * @param int   $item_id
	 * @param array $item
	 */
	public function set_attendee_optout_choice( $item_id, $item ) {
		/** @var \Tribe__Tickets_Plus__Commerce__WooCommerce__Main $commerce_woo */
		$commerce_woo = tribe( 'tickets-plus.commerce.woo' );

		$optout_key = $commerce_woo->attendee_optout_key;

		// If this option is not here just drop
		if ( ! isset( $item[ $optout_key ] ) && ! isset( $item['attendee_optout'] ) ) {
			return;
		}

		$optout = 'no';

		if ( isset( $item[ $optout_key ] ) ) {
			$optout = $item[ $optout_key ];
		} elseif ( isset( $item['attendee_optout'] ) ) {
			$optout = $item['attendee_optout'];
		}

		$optout = filter_var( $optout, FILTER_VALIDATE_BOOLEAN );
		$optout = $optout ? 'yes' : 'no';

		wc_add_order_item_meta( $item_id, $this->attendee_optout_key, $optout );
	}

	/**
	 * Store the attendee optout value for each order item.
	 *
	 * This method should only be used if a version of WooCommerce greater than or equal
	 * to 3.0 is in use.
	 *
	 * @since 4.4.6
	 *
	 * @param WC_Order_Item $item
	 * @param string        $cart_item_key
	 * @param array         $values
	 */
	public function set_attendee_optout_value( $item, $cart_item_key, $values ) {
		/** @var \Tribe__Tickets_Plus__Commerce__WooCommerce__Main $commerce_woo */
		$commerce_woo = tribe( 'tickets-plus.commerce.woo' );

		$optout_key = $commerce_woo->attendee_optout_key;

		// If this option is not here just drop
		if ( ! isset( $values[ $optout_key ] ) && ! isset( $values['attendee_optout'] ) ) {
			return;
		}

		$optout = 'no';

		if ( isset( $values[ $optout_key ] ) ) {
			$optout = $values[ $optout_key ];
		} elseif ( isset( $values['attendee_optout'] ) ) {
			$optout = $values['attendee_optout'];
		}

		$optout = filter_var( $optout, FILTER_VALIDATE_BOOLEAN );
		$optout = $optout ? 'yes' : 'no';

		$item->add_meta_data( $this->attendee_optout_key, $optout );
	}

	/**
	 * Hide the Attendee Output Choice in the Order Page
	 *
	 * @param array $order_items The order items.
	 *
	 * @return array
	 */
	public function hide_attendee_optout_choice( $order_items ) {
		$order_items[] = $this->attendee_optout_key;

		return $order_items;
	}

	/**
	 * Orders report object accessor method
	 *
	 * @return Tribe__Tickets_Plus__Commerce__WooCommerce__Orders__Report
	 */
	public function orders_report() {
		static $report;

		if ( ! $report instanceof self ) {
			$report = new Tribe__Tickets_Plus__Commerce__WooCommerce__Orders__Report;
		}

		return $report;
	}

	/**
	 * Custom meta integration object accessor method
	 *
	 * @since 4.1
	 *
	 * @return Tribe__Tickets_Plus__Commerce__WooCommerce__Meta
	 */
	public function meta() {
		if ( ! self::$meta ) {
			self::$meta = new Tribe__Tickets_Plus__Commerce__WooCommerce__Meta;
		}

		return self::$meta;
	}

	/**
	 * Provides a copy of the global stock integration object.
	 *
	 * @since 4.1
	 *
	 * @return Tribe__Tickets_Plus__Commerce__WooCommerce__Global_Stock
	 */
	public function global_stock() {
		if ( ! self::$global_stock ) {
			self::$global_stock = new Tribe__Tickets_Plus__Commerce__WooCommerce__Global_Stock;
		}

		return self::$global_stock;
	}

	public function settings() {
		if ( empty( $this->settings ) ) {
			$this->settings = new Tribe__Tickets_Plus__Commerce__WooCommerce__Settings;
		}

		return $this->settings;
	}

	/**
	 * Enqueue the plugin stylesheet(s).
	 *
	 * @author caseypicker
	 * @since 3.9
	 * @return void
	 */
	public function enqueue_styles() {
		//Only enqueue wootickets styles on singular event page
		if ( is_singular( Tribe__Tickets__Main::instance()->post_types() ) ) {
			wp_enqueue_style( 'TribeEventsWooTickets' );
			wp_enqueue_style( 'tribe-events-wootickets-override-style' );
		}
	}

	public function admin_enqueue_styles() {
		wp_enqueue_style( 'TribeEventsWooTickets' );
		wp_enqueue_style( 'tribe-events-wootickets-override-style' );
	}

	/**
	 * Where the cart form should lead the users into
	 *
	 * @since 4.8.1
	 *
	 * @return string
	 */
	public function get_cart_url() {
		/** @var \Tribe__Tickets_Plus__Commerce__Woocommerce__Cart $cart */
		$cart = tribe( 'tickets-plus.commerce.woo.cart' );

		return $cart->get_cart_url();
	}

	/**
	 * Get the checkout URL.
	 *
	 * @since 4.11.0
	 *
	 * @return string Checkout URL.
	 */
	public function get_checkout_url() {
		/** @var \Tribe__Tickets_Plus__Commerce__Woocommerce__Cart $cart */
		$cart = tribe( 'tickets-plus.commerce.woo.cart' );

		return $cart->get_checkout_url();
	}

	/**
	 * Adds cart url to list used for localized variables.
	 *
	 * @since 4.11.0
	 *
	 * @param array $urls The original array.
	 * @return array
	 */
	public function add_cart_url( $urls = [] ) {
		$cart_url = $this->get_cart_url();
		$urls[ __CLASS__ ]   = $cart_url;

		return $urls;
	}

	/**
	 * Adds checkout url to list used for localized variables.
	 *
	 * @since 4.11.0
	 *
	 * @param array $urls The original array.
	 * @return array
	 */
	public function add_checkout_url( $urls = [] ) {
		/** @var \Tribe__Tickets_Plus__Commerce__Woocommerce__Cart $cart */
		$cart = tribe( 'tickets-plus.commerce.woo.cart' );

		$urls[ __CLASS__ ] = $cart->get_checkout_url();

		return $urls;
	}

	/**
	 * If a ticket is edited via the WooCommerce product editor (vs the ticket meta
	 * box exposed in the event editor) then we need to trigger an update to ensure
	 * cost meta in particular stays up-to-date on our side.
	 *
	 * @param int $product_id The product ID.
	 */
	public function syncronize_product_editor_changes( $product_id ) {
		$event = $this->get_event_for_ticket( $product_id );

		// This product is not connected with an event
		if ( ! $event ) {
			return;
		}

		// Trigger an update
		Tribe__Events__API::update_event_cost( $event->ID );
	}

	/**
	 * When a user deletes a ticket (product) we want to store
	 * a copy of the product name, so we can show it in the
	 * attendee list for an event.
	 *
	 * @param int|WP_Post $post
	 */
	public function handle_delete_post( $post ) {
		if ( is_numeric( $post ) ) {
			$post = WP_Post::get_instance( $post );
		}

		if ( ! $post instanceof WP_Post ) {
			return false;
		}

		// Bail if it's not a Product
		if ( 'product' !== $post->post_type ) {
			return;
		}

		// Bail if the product is not a Ticket
		$event = get_post_meta( $post->ID, $this->event_key, true );

		if ( ! $event ) {
			return;
		}

		$attendees = $this->get_attendees_by_id( $event );

		foreach ( (array) $attendees as $attendee ) {
			if ( $attendee['product_id'] == $post->ID ) {
				update_post_meta( $attendee['attendee_id'], $this->deleted_product, esc_html( $post->post_title ) );
			}
		}
	}

	/**
	 * Add a custom email handler to WooCommerce email system
	 *
	 * @param array $classes of WC_Email objects
	 *
	 * @return array of WC_Email objects
	 */
	public function add_email_class_to_woocommerce( $classes ) {

		if ( ! class_exists( 'Tribe__Tickets_Plus__Commerce__WooCommerce__Email' ) ) {
			return $classes;
		}

		$this->mailer                          = new Tribe__Tickets_Plus__Commerce__WooCommerce__Email();
		$classes['Tribe__Tickets__Woo__Email'] = $this->mailer;

		return $classes;
	}

	/**
	 * Register our custom post type
	 */
	public function register_wootickets_type() {
		$args = [
			'label'           => esc_html( tribe_get_ticket_label_plural( 'woo_post_type_label' ) ),
			'public'          => false,
			'show_ui'         => false,
			'show_in_menu'    => false,
			'query_var'       => false,
			'rewrite'         => false,
			'capability_type' => 'post',
			'has_archive'     => false,
			'hierarchical'    => true,
		];

		register_post_type( self::ATTENDEE_OBJECT, $args );
	}

	public static function is_wc_paypal_gateway_active() {
		// Add PayPal delay settings if PayPal is active and enabled.
		$woo_gateways = WC()->payment_gateways->get_available_payment_gateways();
		$pp_gateways  = [ 'paypal', 'ppec_paypal' ];

		foreach ( $pp_gateways as $gateway ) {
			// check if gateway exists and is enabled
			if ( ! empty( $woo_gateways[ $gateway ] ) && $woo_gateways[ $gateway ]->enabled ) {
				return true;
			}
		}

		return false;
	}

	/**
	 * Checks if a Order has Tickets
	 *
	 * @since 5.9.1 Updated retrieval of meta data to use WooCommece ORM.
	 *
	 * @param int $order_id
	 *
	 * @return boolean
	 */
	public function order_has_tickets( $order_id ) {
		$has_tickets = false;
		$order       = wc_get_order( $order_id );

		if ( ! $order ) {
			return;
		}

		$done = $order->get_meta(
			$this->order_has_tickets,
			true
		);

		/**
		 * get_post_meta returns empty string when the meta doesn't exists
		 * in support 2 possible values:
		 * - Empty string which will do the logic using WC_Order below
		 * - Cast boolean the return of the get_post_meta
		 */
		if ( '' !== $done ) {
			return (bool) $done;
		}

		$order_items = $order->get_items();

		// Bail if the order is empty
		if ( empty( $order_items ) ) {
			return $has_tickets;
		}

		// Iterate over each product
		foreach ( (array) $order_items as $item_id => $item ) {
			$product_id = isset( $item['product_id'] ) ? $item['product_id'] : $item['id'];
			// Get the event this tickets is for
			$post_id = get_post_meta( $product_id, $this->event_key, true );

			if ( ! empty( $post_id ) ) {

				$has_tickets = true;
				break;
			}
		}

		return $has_tickets;
	}

	/**
	 * Adds a 5-second delay to ticket generation to help protect against race conditions.
	 *
	 * @since 4.9.2
	 *
	 * @param int    $order_id The id of the ticket order.
	 * @param string $unused_from     Deprecated. The status the order is transitioning from.
	 * @param string $unused_to       Deprecated. The status the order is transitioning to.
	 */
	public function delayed_ticket_generation( $order_id, $unused_from = null, $unused_to = null ) {
		// If we're not using PayPal, we don't need to delay, just generate tickets
		if ( 'paypal' !== strtolower( get_post_meta( $order_id, '_payment_method', true ) ) ) {
			$this->generate_tickets( $order_id );
			return;
		}

		/**
		 * Allows users to customize the delay put in place
		 *
		 * @since 4.9.2
		 *
		 * @param string A date/time string. Note it must be positive!
		 */
		$ticket_delay = apply_filters( 'tribe_ticket_generation_delay', '+5 seconds', $order_id );
		$timestamp = strtotime( $ticket_delay );

		// In case the timestamp is borked, fall back to 5 seconds
		if ( ! $timestamp || $timestamp < time() ) {
			$timestamp = strtotime( '+5 seconds' );
		}

		if ( self::is_wc_paypal_gateway_active() && 'immediate' !== tribe_get_option( 'tickets-woo-paypal-delay', 'delay' ) ) {
			wp_schedule_single_event( $timestamp, 'tribe_wc_delayed_ticket_generation', [ $order_id ] );
		} else {
			$this->generate_tickets( $order_id );
		}
	}

	/**
	 * Generates the tickets.
	 *
	 * This happens only once, as soon as an order reaches a suitable status (which is set in
	 * the WooCommerce-specific ticket settings).
	 *
	 * @since 5.9.1 Updated logic to new WooCommerce HPOS requirement.
	 *
	 * @param int $order_id
	 */
	public function generate_tickets( $order_id ) {
		$order = wc_get_order( $order_id );
		if ( empty( $order ) || ! $order instanceof WC_Order ) {
			return;
		}

		$order_items = $order->get_items();

		// Bail if the order is empty.
		if ( empty( $order_items ) ) {
			return;
		}

		/**
		 * Hook before WooCommerce Tickets are generated in Event Tickets Plus.
		 *
		 * @since 4.10.4
		 *
		 * @param int $order_id The order ID.
		 */
		do_action( 'tribe_tickets_plus_woo_before_generate_tickets', $order_id );

		$order_status = $order->get_status();

		// Store the statuses cleanup.
		$clean_up_statuses = static function ( $status ) {
			return str_replace( 'wc-', '', $status );
		};

		// Fetch from DB.
		$dispatch_statuses   = (array) tribe_get_option( 'tickets-woo-dispatch-status', $this->settings()->get_default_ticket_dispatch_statuses() );
		$generation_statuses = (array) tribe_get_option( 'tickets-woo-generation-status', $this->settings()->get_default_ticket_generation_statuses() );

		// Cleanup execution.
		$dispatch_statuses   = array_map( $clean_up_statuses, $dispatch_statuses );
		$generation_statuses = array_map( $clean_up_statuses, $generation_statuses );

		/**
		 * Filters the list of ticket order stati that should trigger the ticket generation.
		 *
		 * By default, the WooCommerced default ones that will affect the ticket stock.
		 *
		 * @since 4.2
		 * @since 6.3.0 Added the $order parameter.
		 *
		 * @param array<string> $generation_statuses The list of order post statuses that should trigger the ticket generation.
		 * @param WC_Order      $order The order object.
		 */
		$generation_statuses = (array) apply_filters( 'event_tickets_woo_ticket_generating_order_stati', $generation_statuses, $order );

		/**
		 * Controls the list of order post statuses used to trigger dispatch of ticket emails.
		 *
		 * @since 4.2
		 * @since 6.3.0 Added the $order parameter.
		 *
		 * @param array<string> $trigger_statuses order post statuses.
		 * @param WC_Order      $order The order object.
		 */
		$dispatch_statuses = apply_filters( 'event_tickets_woo_complete_order_stati', $dispatch_statuses, $order );

		$should_generate    = in_array( $order_status, $generation_statuses, true ) || in_array( 'immediate', $generation_statuses, true );
		$should_dispatch    = in_array( $order_status, $dispatch_statuses, true ) || in_array( 'immediate', $dispatch_statuses, true );
		$already_generated  = $order->get_meta( $this->order_has_tickets, true );
		$already_dispatched = $order->get_meta( $this->mail_sent_meta_key, true );

		$has_tickets       = false;
		$created_attendees = false;

		$customer_id    = $order->get_customer_id();
		$customer_email = $order->get_billing_email();
		$customer_name  = $order->get_billing_first_name() . ' ' . $order->get_billing_last_name();

		// Iterate over each product
		foreach ( (array) $order_items as $item_id => $item ) {
			// order attendee IDs in the meta are per ticket type
			$order_attendee_id = 0;

			$product = $this->get_product_from_item( $order, $item );

			if ( empty( $product ) ) {
				continue;
			}

			$product_id = $this->get_product_id( $product );

			$ticket = $this->get_ticket( null, $product_id );

			// Only process our own tickets.
			if ( ! $ticket ) {
				continue;
			}

			// Check if the order item contains attendee optout information
			$optout_data = isset( $item['item_meta'][ $this->attendee_optout_key ] )
				? $item['item_meta'][ $this->attendee_optout_key ]
				: false;

			$optout = is_array( $optout_data )
				? reset( $optout_data ) // WC 2.x
				: $optout_data;         // WC 3.x

			$optout = filter_var( $optout, FILTER_VALIDATE_BOOLEAN );
			$optout = (int) $optout;

			// Get the event this ticket is for
			$post_id = $ticket->get_event_id();

			$quantity = empty( $item['qty'] ) ? 0 : (int) $item['qty'];

			if ( ! empty( $post_id ) ) {
				$has_tickets = true;

				if ( $already_generated || ! $should_generate ) {
					break;
				}

				/** @var Tribe__Tickets__Commerce__Currency $currency */
				$currency        = tribe( 'tickets.commerce.currency' );
				$currency_symbol = $currency->get_currency_symbol( $product_id, true );

				// Iterate over all the amount of tickets purchased (for this product).
				for ( $i = 0; $i < $quantity; $i++ ) {
					/**
					 * Allow filtering the individual attendee name used when creating a new attendee.
					 *
					 * @since 5.1.0
					 *
					 * @param string                  $individual_attendee_name The attendee full name.
					 * @param int|null                $attendee_number          The attendee number index value from the order, starting with zero.
					 * @param int                     $order_id                 The order ID.
					 * @param int                     $ticket_id                The ticket ID.
					 * @param int                     $post_id                  The ID of the post associated to the ticket.
					 * @param Tribe__Tickets__Tickets $provider                 The current ticket provider object.
					 */
					$individual_attendee_name = apply_filters( 'tribe_tickets_attendee_create_individual_name', $customer_name, $i, $order_id, $product_id, $post_id, $this );

					/**
					 * Allow filtering the individual attendee email used when creating a new attendee.
					 *
					 * @since 5.1.0
					 *
					 * @param string                  $individual_attendee_email The attendee email.
					 * @param int|null                $attendee_number           The attendee number index value from the order, starting with zero.
					 * @param int                     $order_id                  The order ID.
					 * @param int                     $ticket_id                 The ticket ID.
					 * @param int                     $post_id                   The ID of the post associated to the ticket.
					 * @param Tribe__Tickets__Tickets $provider                  The current ticket provider object.
					 */
					$individual_attendee_email = apply_filters( 'tribe_tickets_attendee_create_individual_email', $customer_email, $i, $order_id, $product_id, $post_id, $this );

					$attendee_data = [
						'title'             => $order_id . ' | ' . $individual_attendee_name . ' | ' . ( $i + 1 ),
						'full_name'         => $individual_attendee_name,
						'email'             => $individual_attendee_email,
						'ticket_id'         => $product_id,
						'order_id'          => $order_id,
						'order_item_id'     => $item_id,
						'order_attendee_id' => $order_attendee_id,
						'post_id'           => $post_id,
						'optout'            => $optout,
						'price_paid'        => $this->get_price_value( $product_id ),
						'price_currency'    => $currency_symbol,
						'user_id'           => $customer_id,
					];

					$created = $this->create_attendee( $ticket, $attendee_data );

					if ( $created ) {
						$created_attendees = true;
					}

					$order_attendee_id++;
				}
			}

			if ( ! $already_generated && $should_generate ) {
				if ( ! isset( $quantity ) ) {
					$quantity = null;
				}
				/**
				 * Action fired when a WooCommerce has had attendee tickets generated for it
				 *
				 * @param int $product_id RSVP ticket post ID
				 * @param int $order_id   ID of the WooCommerce order
				 * @param int $quantity   Quantity ordered
				 * @param int $post_id    ID of event
				 */
				do_action( 'event_tickets_woocommerce_tickets_generated_for_product', $product_id, $order_id, $quantity, $post_id );

				$order->update_meta_data( $this->order_has_tickets, '1' );
				$order->save_meta_data();
			}
		}

		// Disallow the dispatch of emails before attendees have been created
		$attendees_generated = $already_generated || $created_attendees;

		if ( $has_tickets && $attendees_generated && ! $already_dispatched && $should_dispatch ) {
			$this->complete_order( $order_id );
		}

		if ( ! $already_generated && $should_generate ) {
			/**
			 * Action fired when a WooCommerce attendee tickets have been generated
			 *
			 * @param int $order_id ID of the WooCommerce order
			 */
			do_action( 'event_tickets_woocommerce_tickets_generated', $order_id );
		}

		/**
		 * Fires after all required Attendees for a WooCommerce order have been created.
		 *
		 * Differently from the other actions, this one is fired even if no Attendees were created.
		 *
		 * @since 5.7.4
		 *
		 * @param int $order_id ID of the WooCommerce order
		 */
		do_action( 'tec_tickets_plus_woo_generated_tickets', $order_id );
	}

	/**
	 * Watches to see if the email being generated is a customer order email and sets up
	 * the addition of ticket-specific messaging if it is.
	 *
	 * @param string $heading The heading.
	 * @param object $email   The email object.
	 */
	public function maybe_add_tickets_msg_to_email( $heading, $email = null ) {
		// If the email object wasn't passed, go no further
		if ( null === $email ) {
			return;
		}

		// Do nothing unless this is a "customer_*" type email
		if ( ! isset( $email->id ) || 0 !== strpos( $email->id, 'customer_' ) ) {
			return;
		}

		// Do nothing if this is a refund notification
		if ( false !== strpos( $email->id, 'refunded' ) ) {
			return;
		}

		// Setup our tickets advisory message
		add_action( 'woocommerce_email_after_order_table', [ $this, 'add_tickets_msg_to_email' ], 10, 2 );
	}

	/**
	 * Adds a message to WooCommerce's order email confirmation.
	 *
	 * @param WC_Order $order The order object.
	 */
	public function add_tickets_msg_to_email( $order ) {
		$order_items = $order->get_items();

		// Bail if the order is empty
		if ( empty( $order_items ) ) {
			return;
		}

		$has_tickets = false;

		// Iterate over each product
		foreach ( (array) $order_items as $item ) {

			$product_id = isset( $item['product_id'] ) ? $item['product_id'] : $item['id'];

			// Get the event this tickets is for
			$post_id = get_post_meta( $product_id, $this->event_key, true );

			if ( ! empty( $post_id ) ) {
				$has_tickets = true;
				break;
			}
		}

		if ( ! $has_tickets ) {
			return;
		}

		echo '<br/>';

		/**
		 * WooCommerce ticket email content.
		 *
		 * @todo Deprecate filter because of outdated filter name prefix.
		 *
		 * @param string $message The email message.
		 *
		 * @return string
		 */
		echo apply_filters(
			'wootickets_email_message',
			esc_html(
				sprintf(
					__( "You'll receive your %s in another email.", 'event-tickets-plus' ),
					tribe_get_ticket_label_plural_lowercase( 'woo_email_confirmation' )
				)
			)
		);
	}

	/**
	 * Saves a ticket (WooCommerce product).
	 *
	 * @param int                           $post_id  Post ID.
	 * @param Tribe__Tickets__Ticket_Object $ticket   Ticket object.
	 * @param array                         $raw_data Ticket data.
	 *
	 * @throws WC_Data_Exception Throws exception when invalid data is found.
	 * @return int|bool
	 */
	public function save_ticket( $post_id, $ticket, $raw_data = [] ) {
		// Run anything we might need on parent method.
		parent::save_ticket( $post_id, $ticket, $raw_data );

		// assume we are updating until we find out otherwise
		$save_type = 'update';

		if ( empty( $ticket->ID ) ) {
			$save_type = 'create';

			/* Create main product post */
			$args = [
				'post_status'  => 'publish',
				'post_type'    => 'product',
				'post_author'  => get_current_user_id(),
				'post_excerpt' => $ticket->description,
				'post_title'   => $ticket->name,
				'menu_order'   => $ticket->menu_order ?? tribe_get_request_var( 'menu_order', -1 ),
				'meta_input'   => [
					'_type' => $raw_data['ticket_type'] ?? 'default',
				],
			];

			$ticket->ID = wp_insert_post( $args );
			$product    = wc_get_product( $ticket->ID );

			if ( ! $product ) {
				return false;
			}

			$product->set_sale_price( $raw_data['ticket_sale_price'] ?? '' );
			$product->set_total_sales( 0 );
			$product->set_tax_status( 'taxable' );
			$product->set_tax_class( '' );
			$product->set_virtual( true );
			$product->set_catalog_visibility( 'hidden' );
			$product->set_downloadable( false );
			$product->set_purchase_note( '' );
			$product->set_weight( '' );
			$product->set_length( '' );
			$product->set_height( '' );
			$product->set_width( '' );
			$product->set_attributes( [] );
			$product->set_date_on_sale_from( ! empty( $raw_data['ticket_sale_start_date'] ) ? gmdate( 'Y-m-d 00:00:00', strtotime( wc_clean( $raw_data['ticket_sale_start_date'] ) ) ) : '' );
			$product->set_date_on_sale_to( ! empty( $raw_data['ticket_sale_end_date'] ) ? gmdate( 'Y-m-d 23:59:59', strtotime( wc_clean( $raw_data['ticket_sale_end_date'] ) ) ) : '' );
			$product->save();

			// Relate event <---> ticket
			add_post_meta( $ticket->ID, $this->event_key, $post_id );
		} else {
			$args = [
				'ID'           => $ticket->ID,
				'post_excerpt' => $ticket->description,
				'post_title'   => $ticket->name,
				'menu_order'   => $ticket->menu_order,
				'meta_input' => [
					'_type' => $raw_data['ticket_type'] ?? 'default',
				]
			];

			$ticket->ID = wp_update_post( $args );

			$product = wc_get_product( $ticket->ID );
			$product->set_date_on_sale_from( ! empty( $raw_data['ticket_sale_start_date'] ) ? gmdate( 'Y-m-d 00:00:00', strtotime( wc_clean( $raw_data['ticket_sale_start_date'] ) ) ) : '' );
			$product->set_date_on_sale_to( ! empty( $raw_data['ticket_sale_end_date'] ) ? gmdate( 'Y-m-d 23:59:59', strtotime( wc_clean( $raw_data['ticket_sale_end_date'] ) ) ) : '' );
			$product->set_sale_price( $raw_data['ticket_sale_price'] ?? '' );
			$product->save();
		}

		if ( ! $ticket->ID ) {
			return false;
		}

		/**
		 * Toggle filter to allow skipping the automatic SKU generation.
		 *
		 * @param bool $should_default_ticket_sku Whether to default the ticket SKU.
		 */
		$should_default_ticket_sku = apply_filters( 'event_tickets_woo_should_default_ticket_sku', true );
		if ( $should_default_ticket_sku ) {
			// make sure the SKU is set to the correct value
			if ( ! empty( $raw_data['ticket_sku'] ) ) {
				$sku = $raw_data['ticket_sku'];
			} else {
				$post_author = get_post_field( 'post_author', $ticket->ID );
				$ticket_name = $raw_data['ticket_name'] ?? $ticket->name;
				$str         = tribe_strtoupper( $ticket_name );
				$sku         = "{$ticket->ID}-{$post_author}-" . str_replace( ' ', '-', $str );
			}

			update_post_meta( $ticket->ID, '_sku', $sku );
		}

		/** @var Tribe__Tickets__Tickets_Handler $tickets_handler */
		$tickets_handler = tribe( 'tickets.handler' );

		// Updates if we should show Description
		$ticket->show_description = isset( $ticket->show_description ) && tribe_is_truthy( $ticket->show_description ) ? 'yes' : 'no';

		update_post_meta( $ticket->ID, $tickets_handler->key_show_description, $ticket->show_description );

		/**
		 * Allow for the prevention of updating ticket price on update.
		 *
		 * @param bool    $can_update_ticket_price Whether the ticket price can be updated.
		 * @param WP_Post $ticket                  The ticket object.
		 */
		$can_update_ticket_price = apply_filters( 'tribe_tickets_can_update_ticket_price', true, $ticket );

		if ( $can_update_ticket_price ) {
			$product->set_regular_price( $ticket->price );

			// Do not update _price if the ticket is on sale: the user should edit this in the WC product editor
			if ( ! wc_get_product( $ticket->ID )->is_on_sale() || 'create' === $save_type ) {
				$product->set_price( $ticket->price );
			}
		}

		// Save the changes to product.
		$product->save();

		// Fetches all Ticket Form Datas
		$data = Arr::get( $raw_data, 'tribe-ticket', [] );

		// Before merging with defaults check the stock data provided
		$stock_provided = ! empty( $data['stock'] ) && '' !== trim( $data['stock'] );

		// By default it is an Unlimited Stock without Global stock
		$defaults = [
			'mode' => 'own',
		];

		$data = wp_parse_args( $data, $defaults );

		// Sanitize Mode
		$data['mode'] = tec_sanitize_string( $data['mode'] );

		// Fetch the Global stock Instance for this Event
		/**
		 * @var Tribe__Tickets__Global_Stock $event_stock
		 */
		$event_stock = new Tribe__Tickets__Global_Stock( $post_id );

		// Only need to do this if we haven't already set one - they shouldn't be able to edit it from here otherwise
		if ( ! $event_stock->is_enabled() ) {
			if ( isset( $data['event_capacity'] ) ) {
				$data['event_capacity'] = trim( tec_sanitize_string( $data['event_capacity'] ) );

				// If empty we need to modify to -1
				if ( '' === $data['event_capacity'] ) {
					$data['event_capacity'] = -1;
				}

				// Makes sure it's an Int after this point
				$data['event_capacity'] = (int) $data['event_capacity'];

				$tickets_handler->remove_hooks();

				// We need to update event post meta - if we've set a global stock
				$event_stock->enable();
				$event_stock->set_stock_level( $data['event_capacity'], true );

				// Update Event capacity
				update_post_meta( $post_id, $tickets_handler->key_capacity, $data['event_capacity'] );
				update_post_meta( $post_id, $event_stock::GLOBAL_STOCK_ENABLED, 1 );

				$tickets_handler->add_hooks();
			}
		} else {
			// If the Global Stock is configured we pull it from the Event
			$global_capacity        = (int) tribe_tickets_get_capacity( $post_id );
			$data['event_capacity'] = (int) Arr::get( 'event_capacity', $data, 0 );

			if ( ! empty( $data['event_capacity'] ) && $data['event_capacity'] !== $global_capacity ) {
				// Update stock level with $data['event_capacity'].
				$event_stock->set_stock_level( $data['event_capacity'], true );
			} else {
				// Set $data['event_capacity'] with what we know.
				$data['event_capacity'] = $global_capacity;
			}
		}

		// Default Capacity will be 0
		$default_capacity   = 0;
		$is_capacity_passed = true;

		// If we have Event Global stock we fetch that Stock
		if ( $event_stock->is_enabled() ) {
			$default_capacity = $data['event_capacity'];
		}

		// Fetch capacity field, if we don't have it use default (defined above)
		$data['capacity'] = trim( Arr::get( $data, 'capacity', $default_capacity ) );

		// If empty we need to modify to the default
		if ( '' !== $data['capacity'] ) {
			// Makes sure it's an Int after this point
			$data['capacity'] = (int) $data['capacity'];

			// The only available value lower than zero is -1 which is unlimited
			if ( 0 > $data['capacity'] ) {
				$data['capacity'] = -1;
			}

			$default_capacity = $data['capacity'];
		}

		// Fetch the stock if defined, otherwise use Capacity field
		$data['stock'] = trim( Arr::get( $data, 'stock', $default_capacity ) );

		// If empty we need to modify to what every capacity was
		if ( '' === $data['stock'] ) {
			$data['stock'] = $default_capacity;
		}

		// Makes sure it's an Int after this point
		$data['stock'] = (int) $data['stock'];

		// The only available value lower than zero is -1 which is unlimited.
		if ( 0 > $data['stock'] ) {
			$data['stock'] = -1;
		}

		$mode = isset( $data['mode'] ) ? $data['mode'] : 'own';

		if ( '' !== $mode ) {
			if ( 'update' === $save_type ) {
				$totals        = $tickets_handler->get_ticket_totals( $ticket->ID );
				$data['stock'] -= $totals['pending'] + $totals['sold'];
			}

			// In here is safe to check because we don't have unlimited = -1
			$status = ( 0 < $data['stock'] ) ? 'instock' : 'outofstock';

			update_post_meta( $ticket->ID, Tribe__Tickets__Global_Stock::TICKET_STOCK_MODE, $mode );
			update_post_meta( $ticket->ID, '_stock', $data['stock'] );
			update_post_meta( $ticket->ID, '_stock_status', $status );
			update_post_meta( $ticket->ID, '_backorders', 'no' );
			update_post_meta( $ticket->ID, '_manage_stock', 'yes' );

			// Prevent Ticket Capacity from going higher then Event Capacity
			if (
				$event_stock->is_enabled()
				&& Tribe__Tickets__Global_Stock::OWN_STOCK_MODE !== $mode
				&& (
					'' === $data['capacity']
					|| $data['event_capacity'] < $data['capacity']
				)
			) {
				$data['capacity'] = $data['event_capacity'];
			}
		} else {
			// Unlimited Tickets
			// Besides setting _manage_stock to "no" we should remove the associated stock fields if set previously
			update_post_meta( $ticket->ID, '_manage_stock', 'no' );
			delete_post_meta( $ticket->ID, '_stock_status' );
			delete_post_meta( $ticket->ID, '_stock' );
			delete_post_meta( $ticket->ID, Tribe__Tickets__Global_Stock::TICKET_STOCK_CAP );
			delete_post_meta( $ticket->ID, Tribe__Tickets__Global_Stock::TICKET_STOCK_MODE );

			// Set Capacity -1 when we don't have a stock mode, which means unlimited
			$data['capacity'] = -1;
		}

		if ( '' !== $data['capacity'] ) {
			// Update Ticket capacity
			update_post_meta( $ticket->ID, $tickets_handler->key_capacity, $data['capacity'] );
		}

		// Delete total Stock cache
		delete_transient( 'wc_product_total_stock_' . $ticket->ID );

		if ( ! empty( $raw_data['ticket_start_date'] ) ) {
			$start_date = Tribe__Date_Utils::maybe_format_from_datepicker( $raw_data['ticket_start_date'] );

			if ( isset( $raw_data['ticket_start_time'] ) ) {
				$start_date .= ' ' . $raw_data['ticket_start_time'];
			}

			$ticket->start_date = date( Tribe__Date_Utils::DBDATETIMEFORMAT, strtotime( $start_date ) );

			$previous_start_date = get_post_meta( $ticket->ID, $tickets_handler->key_start_date, true );

			// Only update when we are modifying
			if ( $ticket->start_date !== $previous_start_date ) {
				update_post_meta( $ticket->ID, $tickets_handler->key_start_date, $ticket->start_date );
			}
		} else {
			delete_post_meta( $ticket->ID, $tickets_handler->key_start_date );
		}

		if ( ! empty( $raw_data['ticket_end_date'] ) ) {
			$end_date = Tribe__Date_Utils::maybe_format_from_datepicker( $raw_data['ticket_end_date'] );

			if ( isset( $raw_data['ticket_end_time'] ) ) {
				$end_date .= ' ' . $raw_data['ticket_end_time'];
			}

			$ticket->end_date  = date( Tribe__Date_Utils::DBDATETIMEFORMAT, strtotime( $end_date ) );
			$previous_end_date = get_post_meta( $ticket->ID, $tickets_handler->key_end_date, true );

			// Only update when we are modifying
			if ( $ticket->end_date !== $previous_end_date ) {
				update_post_meta( $ticket->ID, $tickets_handler->key_end_date, $ticket->end_date );
			}
		} else {
			delete_post_meta( $ticket->ID, '_ticket_end_date' );
		}

		/** @var Tribe__Tickets__Version $version */
		$version = tribe( 'tickets.version' );

		$version->update( $ticket->ID );

		/**
		 * Generic action fired after saving a ticket (by type)
		 *
		 * @since 4.7
		 *
		 * @param int                           $post_id  Post ID of post the ticket is tied to
		 * @param Tribe__Tickets__Ticket_Object $ticket   Ticket that was just saved
		 * @param array                         $raw_data Ticket data
		 * @param string                        $class    Commerce engine class
		 */
		do_action( 'event_tickets_after_' . $save_type . '_ticket', $post_id, $ticket, $raw_data, __CLASS__ );

		/**
		 * Generic action fired after saving a ticket
		 *
		 * @since 4.7
		 *
		 * @param int                           $post_id  Post ID of post the ticket is tied to
		 * @param Tribe__Tickets__Ticket_Object $ticket   Ticket that was just saved
		 * @param array                         $raw_data Ticket data
		 * @param string                        $class    Commerce engine class
		 */
		do_action( 'event_tickets_after_save_ticket', $post_id, $ticket, $raw_data, __CLASS__ );

		return $ticket->ID;
	}

	/**
	 * Deletes a ticket or an attendee post.
	 *
	 * @since 5.7.4 Fixed an error that was occurring when $tickets was null.
	 * @since 6.7.0 Added a call to decrement the issued tickets count when an attendee is deleted.
	 *
	 * @param int $post_id   The post ID of the event (or post) holding the ticket.
	 * @param int $ticket_id The post ID of the ticket product or the attendee post.
	 *
	 * @return bool
	 */
	public function delete_ticket( $post_id, $ticket_id ) {
		// Ensure we know the event and product IDs. The event ID may not have been passed in.
		$post_id          = (int) ( $post_id ?: get_post_meta( $ticket_id, self::ATTENDEE_EVENT_KEY, true ) );
		$product_id       = (int) get_post_meta( $ticket_id, $this->attendee_product_key, true );
		$ticket_post_type = get_post_type( $ticket_id );

		/**
		 * Use this Filter to choose if you want to delete tickets instead of only trashing them.
		 *
		 * @param bool   $trash_ticket Whether to trash the ticket (true) instead of deleting it (false).
		 * @param int    $ticket_id    The post ID of the ticket product.
		 */
		$trash_ticket = apply_filters( 'tribe_tickets_plus_trash_ticket', true, $ticket_id );

		// Trash or delete the post depending on $trash_ticket.
		if ( $trash_ticket ) {
			$delete = wp_trash_post( $ticket_id );
		} else {
			$delete = wp_delete_post( $ticket_id, true );
		}

		if ( is_wp_error( $delete ) || ! isset( $delete->ID ) ) {
			return false;
		}

		// Increment deleted attendees count if we're deleting an attendee.
		if ( self::ATTENDEE_OBJECT === $ticket_post_type ) {
			Tribe__Tickets__Attendance::instance( $post_id )->increment_deleted_attendees_count();

			// Decrease issued tickets (total sales) count if the product is valid.
			if ( $product_id ) {
				$this->decrement_issued_tickets_count( $product_id );
			}
		}

		// Get the ticket and update product inventory.
		$ticket = $this->get_ticket( $post_id, $product_id );
		if ( $ticket && $ticket->capacity() > $ticket->stock() ) {
			$this->increment_product_inventory( $product_id );
		}

		// Run anything we might need on parent method.
		parent::delete_ticket( $post_id, $ticket_id );

		// Delete event capacity if there are no shared tickets.
		$tickets_handler = tribe( 'tickets.handler' );
		$has_shared_tickets = 0 !== count( $tickets_handler->get_event_shared_tickets( $post_id ) );
		if ( ! $has_shared_tickets ) {
			tribe_tickets_delete_capacity( $post_id );
		}

		do_action( 'wootickets_ticket_deleted', $ticket_id, $post_id, $product_id );

		return true;
	}

	/**
	 * Increments the inventory of the specified product by 1 (or by the optional
	 * $increment_by value).
	 *
	 * @param int $product_id
	 * @param int $increment_by
	 *
	 * @return bool
	 */
	protected function increment_product_inventory( $product_id, $increment_by = 1 ) {
		$product = wc_get_product( $product_id );

		if ( ! $product || ! $product->managing_stock() ) {
			return false;
		}

		// WooCommerce 3.x
		if ( function_exists( 'wc_update_product_stock' ) ) {
			$success = wc_update_product_stock( $product, (int) $product->get_stock_quantity() + $increment_by );
		} // WooCommerce 2.x
		else {
			$success = $product->set_stock( (int) $product->stock + $increment_by );
		}

		return null !== $success;
	}

	/**
	 * Replaces the link to the WooCommerce product with a link to the associated Event
	 * on the order confirmation page, if the product is linked to an Event.
	 *
	 * @since 6.1.5 Added check to ensure only strings are returned.
	 *
	 * @param string  $post_link        The original post link for the WooCommerce product.
	 * @param WP_Post $post             The post object representing the WooCommerce product.
	 * @param bool    $unused_leavename Unused parameter for compatibility.
	 * @param bool    $unused_sample    Unused parameter for compatibility.
	 *
	 * @return string The modified post link pointing to the associated Event,
	 *                or the original post link if no Event is associated or the Event link is invalid.
	 */
	public function hijack_ticket_link( $post_link, $post, $unused_leavename, $unused_sample ) {
		if ( 'product' !== $post->post_type ) {
			return $post_link;
		}

		$event = get_post_meta( $post->ID, $this->event_key, true );
		if ( empty( $event ) ) {
			return $post_link;
		}

		$new_link = get_permalink( $event );

		if ( empty( $new_link ) ) {
			return $post_link;
		}

		return $new_link;
	}

	/**
	 * Shows the tickets form in the front end
	 *
	 * @param string $content The content.
	 *
	 * @return void
	 */
	public function front_end_tickets_form( $content ) {
		$post = $GLOBALS['post'];

		// For recurring events (child instances only), default to loading tickets for the parent event
		if ( ! empty( $post->post_parent ) && function_exists( 'tribe_is_recurring_event' ) && tribe_is_recurring_event( $post->ID ) ) {
			$post = get_post( $post->post_parent );
		}

		$tickets = $this->get_tickets( $post->ID );

		foreach( $tickets as $index => $ticket ) {
			if ( __CLASS__ !== $ticket->provider_class ) {
				unset( $tickets[ $index ] );
			}
		}

		if ( empty( $tickets ) ) {
			return;
		}

		// Check to see if all available tickets' end-sale dates have passed, in which case no form
		// should show on the front-end.
		$expired_tickets = 0;

		foreach ( $tickets as $ticket ) {
			if ( ! $ticket->date_in_range() ) {
				$expired_tickets++;
			}
		}

		$must_login = ! is_user_logged_in() && $this->login_required();

		if ( $expired_tickets >= count( $tickets ) ) {
			/**
			 * Allow to hook into the FE form of the tickets if tickets has already expired. If the action used the
			 * second value for tickets make sure to use a callback instead of an inline call to the method such as:
			 *
			 * Example:
			 *
			 * add_action( 'tribe_tickets_expired_front_end_ticket_form', function( $must_login, $tickets ) {
			 *  Tribe__Tickets_Plus__Attendees_List::instance()->render();
			 * }, 10, 2 );
			 *
			 * If the tickets are not required to be used on the view you an use instead.
			 *
			 * add_action( 'tribe_tickets_expired_front_end_ticket_form', [ Tribe__Tickets_Plus__Attendees_List::instance(), 'render' ] );
			 *
			 * @since 4.7.3
			 *
			 * @param bool    $must_login Whether the user must be logged in.
			 * @param array   $tickets    The tickets.
			 */
			do_action( 'tribe_tickets_expired_front_end_ticket_form', $must_login, $tickets );
		}

		$global_stock_enabled = $this->uses_global_stock( $post->ID );

		/**
		 * Allow for the addition of content (namely the "Who's Attening?" list) above the ticket form.
		 *
		 * @since 4.5.4
		 */
		do_action( 'tribe_tickets_before_front_end_ticket_form' );

		Tribe__Tickets__Tickets_View::instance()->get_tickets_block( $post->ID );
	}

	/**
	 * Grabs the submitted front end tickets form and adds the products
	 * to the cart
	 */
	public function process_front_end_tickets_form() {
		parent::process_front_end_tickets_form();

		global $woocommerce;

		// We just want to process wootickets submissions here.
		if (
			empty( $_REQUEST['wootickets_process'] )
			|| intval( $_REQUEST['wootickets_process'] ) !== 1
			|| empty( $_POST['product_id'] )
		) {
			return;
		}

		foreach ( (array) $_POST['product_id'] as $product_id ) {
			$quantity = isset( $_POST[ 'quantity_' . $product_id ] ) ? (int) $_POST[ 'quantity_' . $product_id ] : 0;
			$optout   = isset( $_POST[ 'optout_' . $product_id ] ) ? $_POST[ 'optout_' . $product_id ] : false;

			/**
			 * Allow hooking into WooCommerce Add to Cart validation.
			 *
			 * Note: This is a WooCommerce filter that is not abstracted for API usage so we have to run it manually.
			 *
			 * @param bool $passed_validation Whether the item can be added to the cart.
			 * @param int  $ticket_id         Ticket ID.
			 * @param int  $quantity          Ticket quantity.
			 */
			$passed_validation = apply_filters( 'woocommerce_add_to_cart_validation', true, $product_id, $quantity );

			$optout = filter_var( $optout, FILTER_VALIDATE_BOOLEAN );
			$optout = $optout ? 'yes' : 'no';

			$cart_data = [
				$this->attendee_optout_key => $optout,
			];

			if ( $passed_validation && $quantity > 0 ) {
				$woocommerce->cart->add_to_cart( $product_id, $quantity, 0, [], $cart_data );
			}
		}

		if ( empty( $_POST[ Tribe__Tickets_Plus__Meta__Storage::META_DATA_KEY ] ) ) {
			return;
		}

		$cart_url = $this->get_cart_url();

		set_transient( $this->get_cart_transient_key(), $cart_url );

		// phpcs:ignore WordPressVIPMinimum.Security.ExitAfterRedirect.NoExit
		wp_safe_redirect( $cart_url );
		tribe_exit();
	}

	/**
	 * Return whether we're currently on the checkout page.
	 *
	 * @return bool
	 */
	public function is_checkout_page() {
		return is_checkout() && ! is_order_received_page();
	}

	/**
	 * Filters the properties to add to a ticket model in the REST API.
	 *
	 * @since 6.8.0
	 *
	 * @param array<string,mixed> $properties Properties to add to the model.
	 * @param WP_Post             $post       The post object.
	 *
	 * @return array<string,mixed>
	 */
	public function filter_pre_build_ticket_properties( array $properties, WP_Post $post ): array {
		if ( 'product' !== $post->post_type ) {
			return $properties;
		}

		$product = wc_get_product( $post->ID );

		if ( ! $product ) {
			return $properties;
		}

		$event_id = get_post_meta( $post->ID, $this->event_key, true );

		$ticket_object = $this->get_ticket( $event_id, $post->ID );

		$start_date = get_post_meta( $post->ID, '_ticket_start_date', true );
		$end_date   = get_post_meta( $post->ID, '_ticket_end_date', true );

		$sale_price = $product->get_sale_price( 'edit' );

		$props = [
			'description'           => $post->post_excerpt,
			'on_sale'               => (bool) $product->is_on_sale( 'edit' ),
			'sale_price'            => $sale_price ? (float) $sale_price : null,
			'price'                 => (float) $product->get_price( 'view' ),
			'regular_price'         => (float) $product->get_regular_price( 'view' ),
			'show_description'      => $ticket_object->show_description(),
			'start_date'            => $start_date,
			'end_date'              => $end_date,
			'sale_price_start_date' => $product->get_date_on_sale_from( 'view' ),
			'sale_price_end_date'   => $product->get_date_on_sale_to( 'view' ),
			'event_id'              => (int) $event_id,
			'manage_stock'          => $product->managing_stock(),
			'stock'                 => $product->get_stock_quantity(),
			'sold'                  => $product->get_total_sales(),
			'sku'                   => $product->get_sku(),
		];

		return array_merge( $properties, $props );
	}

	/**
	 * Gets an individual ticket
	 *
	 * @since 5.7.4 Fixed the `price` and `regular_price` properties to return the correct values.
	 *
	 * @param int $post_id   The post ID.
	 * @param int $ticket_id The ticket ID.
	 *
	 * @return null|Tribe__Tickets__Ticket_Object
	 */
	public function get_ticket( $post_id, $ticket_id ) {
		if (
			empty( $ticket_id )
			|| ! function_exists( 'wc_get_product' )
		) {
			return null;
		}

		$product = wc_get_product( $ticket_id );

		if ( ! $product ) {
			return null;
		}

		$cached = wp_cache_get( (int) $ticket_id, 'tec_tickets' );
		if ( $cached && is_array( $cached ) ) {
			return new Tribe__Tickets__Ticket_Object( $cached );
		}

		$return       = new Tribe__Tickets__Ticket_Object();
		$product_post = get_post( $this->get_product_id( $product ) );
		$qty_sold     = get_post_meta( $ticket_id, 'total_sales', true );

		$return->description   = $product_post->post_excerpt;
		$return->frontend_link = get_permalink( $ticket_id );
		$return->ID            = $ticket_id;
		$return->name          = $product->get_title();
		$return->menu_order    = $product->get_menu_order();
		$return->price         = $this->get_formatted_price( $this->get_price_value_for( $product ) );
		$return->regular_price = $this->get_formatted_price( $product->get_regular_price( 'edit' ) );
		$return->on_sale       = (bool) $product->is_on_sale( 'edit' );
		$return->capacity         = tribe_tickets_get_capacity( $ticket_id );
		$return->provider_class   = get_class( $this );
		$return->admin_link       = admin_url( sprintf( get_post_type_object( $product_post->post_type )->_edit_link . '&action=edit', $ticket_id ) );
		$return->report_link      = $this->get_ticket_reports_link( null, $ticket_id );
		$return->sku              = $product->get_sku();
		$return->show_description = $return->show_description();
		$return->price_suffix     = $this->get_price_suffix( $product, $this->get_price_value_for( $product, $return ), 1 );

		$start_date = get_post_meta( $ticket_id, '_ticket_start_date', true );
		$end_date   = get_post_meta( $ticket_id, '_ticket_end_date', true );

		if ( ! empty( $start_date ) ) {
			$start_date_unix    = strtotime( $start_date );
			$return->start_date = Tribe__Date_Utils::date_only( $start_date_unix, true );
			$return->start_time = Tribe__Date_Utils::time_only( $start_date_unix );
		}

		if ( ! empty( $end_date ) ) {
			$end_date_unix    = strtotime( $end_date );
			$return->end_date = Tribe__Date_Utils::date_only( $end_date_unix, true );
			$return->end_time = Tribe__Date_Utils::time_only( $end_date_unix );
		}

		// If the quantity sold wasn't set, default to zero
		$qty_sold = $qty_sold ? $qty_sold : 0;

		// Ticket stock is a simple reflection of remaining inventory for this item...
		$stock = $product->get_stock_quantity();

		// If we don't have a stock value, then stock should be considered 'unlimited'
		if ( null === $stock ) {
			$stock = -1;
		}

		$return->manage_stock( $product->managing_stock() );
		$return->stock( $stock );
		$return->global_stock_mode( get_post_meta( $ticket_id, Tribe__Tickets__Global_Stock::TICKET_STOCK_MODE, true ) );
		$capped = get_post_meta( $ticket_id, Tribe__Tickets__Global_Stock::TICKET_STOCK_CAP, true );

		if ( '' !== $capped ) {
			$return->global_stock_cap( $capped );
		}

		$return->qty_sold( $qty_sold );
		$return->qty_cancelled( $this->get_cancelled( $ticket_id ) );
		$return->qty_refunded( $this->get_refunded( $ticket_id ) );


		// From Event Tickets 4.4.9 onwards we can supply a callback to calculate the number of
		// pending items per ticket on demand (since determining this is expensive and the data isn't
		// always required, it makes sense not to do it unless required)
		if ( version_compare( Tribe__Tickets__Main::VERSION, '4.4.9', '>=' ) ) {
			$return->qty_pending( [ $this, 'get_qty_pending' ] );
			$qty_pending = $return->qty_pending();

			// Removes pending sales from total sold
			$return->qty_sold( $qty_sold - $qty_pending );
		} else {
			// If an earlier version of Event Tickets is activated we'll need to calculate this up front
			$pending_totals = $this->count_order_items_by_status( $ticket_id, 'incomplete' );
			$return->qty_pending( $pending_totals['total'] ? $pending_totals['total'] : 0 );
		}

		/**
		 * Use this Filter to change any information you want about this ticket
		 *
		 * @param object $ticket    The ticket object.
		 * @param int    $post_id   The post ID.
		 * @param int    $ticket_id The ticket ID.
		 */
		$ticket = apply_filters( 'tribe_tickets_plus_woo_get_ticket', $return, $post_id, $ticket_id );

		if ( $ticket instanceof Tribe__Tickets__Ticket_Object ) {
			// Before caching the ticket, resolve the `qty_pending` callback to a value.
			$ticket->qty_pending( $ticket->qty_pending() );
			wp_cache_set( (int) $ticket->ID, $ticket->to_array(), 'tec_tickets' );
		}

		return $ticket;
	}

	/**
	 * Lazily calculates the quantity of pending sales for the specified ticket.
	 *
	 * @param int  $ticket_id The ticket ID.
	 * @param bool $refresh   Whether to refresh the quantity pending.
	 *
	 * @return int
	 */
	public function get_qty_pending( $ticket_id, $refresh = false ) {
		if ( $refresh || empty( $this->pending_orders_by_ticket[ $ticket_id ] ) ) {
			$pending_totals                               = $this->count_order_items_by_status( $ticket_id, 'incomplete' );
			$this->pending_orders_by_ticket[ $ticket_id ] = $pending_totals['total'] ? $pending_totals['total'] : 0;
		}

		return $this->pending_orders_by_ticket[ $ticket_id ];
	}

	/**
	 * This method is used to lazily set and correct stock levels for tickets which
	 * draw on the global event inventory.
	 *
	 * It's required because, currently, there is a discrepancy between how individual
	 * tickets are created and saved (ie, via ajax) and how event-wide settings such as
	 * global stock are saved - which means a ticket may be saved before the global
	 * stock level and save_tickets() will set the ticket inventory to zero. To avoid
	 * the out-of-stock issues that might otherwise result, we lazily correct this
	 * once the global stock level is known.
	 *
	 * @param int $existing_stock The existing stock.
	 * @param int $post_id        The post ID.
	 * @param int $ticket_id      The ticket ID.
	 *
	 * @return int
	 */
	protected function set_stock_level_for_global_stock_tickets( $existing_stock, $post_id, $ticket_id ) {
		// If this event does not have a global stock then do not modify the existing stock level
		if ( ! $this->uses_global_stock( $post_id ) ) {
			return $existing_stock;
		}

		// If this specific ticket maintains its own independent stock then again do not interfere
		if ( Tribe__Tickets__Global_Stock::OWN_STOCK_MODE === get_post_meta( $ticket_id, Tribe__Tickets__Global_Stock::TICKET_STOCK_MODE, true ) ) {
			return $existing_stock;
		}

		$product = wc_get_product( $ticket_id );

		// Otherwise the ticket stock ought to match the current global stock
		$actual_stock = $product ? $product->get_stock_quantity() : 0;
		$global_stock = $this->global_stock_level( $post_id );

		// Look out for and correct discrepancies where the actual stock is zero but the global stock is non-zero
		if ( 0 == $actual_stock && 0 < $global_stock ) {
			update_post_meta( $ticket_id, '_stock', $global_stock );
			update_post_meta( $ticket_id, '_stock_status', 'instock' );
		}

		return $global_stock;
	}

	/**
	 * Determine the total number of the specified ticket contained in orders which have
	 * progressed to a "completed" or "incomplete" status.
	 *
	 * Essentially this returns the total quantity of tickets held within orders that are
	 * complete or incomplete (incomplete are: "pending", "on hold" or "processing").
	 *
	 * @param int    $ticket_id The ticket ID.
	 * @param string $status    Types of orders: incomplete or complete
	 *
	 * @return array The totals.
	 */
	public function count_order_items_by_status( $ticket_id, $status = 'incomplete' ) {
		/** @var Tribe__Post_Transient $transient */
		$transient = tribe( 'post-transient' );

		$cache_key = self::ORDER_COUNT_CACHE_KEY . '_' . $status;

		$totals = $transient->get( $ticket_id, $cache_key );

		if ( is_array( $totals ) ) {
			return $totals;
		}

		$totals = [
			'total'          => 0,
			'recorded_sales' => 0,
			'reduced_stock'  => 0,
		];

		$incomplete_order_items = $this->get_orders_by_status( $ticket_id, $status );

		$order_ids      = wp_list_pluck( $incomplete_order_items, 'order_id' );
		$order_item_ids = wp_list_pluck( $incomplete_order_items, 'order_item_id' );

		$order_meta      = $this->get_orders_meta( $order_ids );
		$order_item_meta = $this->get_order_items_meta( $order_item_ids );

		foreach ( $incomplete_order_items as $item ) {
			$order_item_id = (int) $item->order_item_id;
			$order_id      = (int) $item->order_id;

			$has_recorded_sales = false;
			$has_reduced_stock  = false;

			if ( empty( $order_item_meta[ $order_item_id ] ) ) {
				continue;
			}

			$quantity = (int) $order_item_meta[ $order_item_id ]->_qty;

			if ( ! empty( $order_meta[ $order_id ] ) ) {
				$has_recorded_sales = filter_var( $order_meta[ $order_id ]->_recorded_sales, FILTER_VALIDATE_BOOLEAN );
				$has_reduced_stock  = filter_var( $order_meta[ $order_id ]->_order_stock_reduced, FILTER_VALIDATE_BOOLEAN );
			}

			$totals['total'] += $quantity;

			if ( $has_recorded_sales ) {
				$totals['recorded_sales'] += $quantity;
			}

			if ( $has_reduced_stock ) {
				$totals['reduced_stock'] += $quantity;
			}
		}

		$transient->set( $ticket_id, $cache_key, $totals, WEEK_IN_SECONDS );

		return $totals;
	}

	/**
	 * Get list of order items for ticket ID by order status.
	 *
	 * @since 5.9.1 Updated logic to new WooCommerce HPOS requirement.
	 * @since 5.9.3 Refactored WooCommerce ORM to use direct query.
	 *
	 * @param int    $ticket_id Ticket ID.
	 * @param string $status    Order status.
	 *
	 * @return array List of order items.
	 */
	protected function get_orders_by_status( $ticket_id, $status = 'incomplete' ) {
		global $wpdb;

		$status_list = $this->incomplete_order_states();

		if ( 'incomplete' !== $status ) {
			// If the status is not incomplete generate a list of all other status.
			$all_statuses        = array_keys( wc_get_order_statuses() );
			$incomplete_statuses = explode( ',', str_replace( "'", '', $status_list ) );
			$remaining_statuses  = array_diff( $all_statuses, $incomplete_statuses );
			$status_list         = array_map(
				function ( $v ) {
					return "'" . esc_sql( $v ) . "'";
				},
				$remaining_statuses
			);
			$status_list         = implode( ',', $status_list );
		}

		$ticket_id = intval( $ticket_id ); // Ensure $ticket_id is an integer.

		if ( ! self::is_hpos_enabled() ) {
			$query = $wpdb->prepare(
				"SELECT woim.order_item_id,p.id as order_id
					FROM {$wpdb->prefix}woocommerce_order_itemmeta AS woim
					INNER JOIN {$wpdb->prefix}woocommerce_order_items AS woi ON woim.order_item_id = woi.order_item_id
					INNER JOIN {$wpdb->prefix}posts AS p ON p.ID = woi.order_id
					WHERE woim.meta_key IN ('_product_id', '_variation_id')
					AND p.post_status IN ({$status_list})
					and woim.meta_value = %s",
				$ticket_id
			);
		} else {
			// For HPOS.
			$query = $wpdb->prepare(
				"SELECT o.id as order_id, oi.order_item_id
					FROM {$wpdb->prefix}wc_order_product_lookup opl
					INNER JOIN {$wpdb->prefix}wc_orders o ON opl.order_id = o.id
					inner join {$wpdb->prefix}woocommerce_order_items oi on oi.order_id = o.id
					WHERE o.type = 'shop_order'
					AND o.status IN ({$status_list})
					AND opl.product_id = %d",
				$ticket_id
			);
		}
		// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.NotPrepared
		return $wpdb->get_results( $query );
	}

	/**
	 * Returns a comma separated list of term IDs representing incomplete order
	 * states.
	 *
	 * @return string
	 */
	protected function incomplete_order_states() {
		$considered_incomplete = (array) apply_filters( 'wootickets_incomplete_order_states', [
			'wc-on-hold',
			'wc-pending',
			'wc-processing',
		] );

		foreach ( $considered_incomplete as &$incomplete ) {
			$incomplete = "'" . $incomplete . "'";
		}

		return implode( ', ', $considered_incomplete );
	}

	/**
	 * Get list of Order Meta for specific Order IDs.
	 *
	 * @since 4.11.0.2
	 * @since 5.9.1 Updated logic to new WooCommerce HPOS requirement.
	 *
	 * @param array $order_ids List of Order IDs.
	 *
	 * @return array List of Order Meta.
	 */
	protected function get_orders_meta( $order_ids ) {
		if ( empty( $order_ids ) ) {
			return [];
		}

		$order_meta = [];

		foreach ( $order_ids as $order_id ) {
			$order = wc_get_order( $order_id );

			if ( $order ) {
				$data                       = new stdClass();
				$data->order_id             = $order->get_id();
				$data->_recorded_sales      = $order->get_meta(
					'_recorded_sales',
					true
				);
				$data->_order_stock_reduced = $order->get_meta(
					'_order_stock_reduced',
					true
				);

				$order_meta[ (int) $order->get_id() ] = $data;
			}
		}

		return $order_meta;
	}

	/**
	 * Get list of Order Item Meta for specific Order Item IDs.
	 *
	 * @since 4.11.0.2
	 * @since 5.9.1 Updated logic to new WooCommerce HPOS requirement.
	 *
	 * @param array $order_item_ids List of Order Item IDs.
	 *
	 * @return array List of Order Item Meta.
	 */
	protected function get_order_items_meta( $order_item_ids ) {
		if ( empty( $order_item_ids ) ) {
			return [];
		}

		$order_item_meta = [];

		foreach ( $order_item_ids as $item_id ) {
			$item = WC_Order_Factory::get_order_item( $item_id );

			if ( ! $item || ! ( $item instanceof WC_Order_Item_Product ) ) {
				continue;
			}

			$data                = new stdClass();
			$data->order_item_id = $item->get_id();
			$data->_qty          = $item->get_quantity();

			$order_item_meta[ (int) $item->get_id() ] = $data;
		}

		return $order_item_meta;
	}

	/**
	 * Accepts a reference to a product (either an object or a numeric ID) and
	 * tests to see if it functions as a ticket: if so, the corresponding event
	 * object is returned. If not, boolean false is returned.
	 *
	 * @param object|int $ticket_product The ticket product.
	 *
	 * @return bool|WP_Post
	 */
	public function get_event_for_ticket( $ticket_product ) {
		if ( is_object( $ticket_product ) && isset( $ticket_product->ID ) ) {
			$ticket_product = $ticket_product->ID;
		}

		if ( null === get_post( $ticket_product ) ) {
			return false;
		}

		$event = get_post_meta( $ticket_product, $this->event_key, true );

		if ( empty( $event ) ) {
			return false;
		}

		if ( in_array( get_post_type( $event ), Tribe__Tickets__Main::instance()->post_types() ) ) {
			return get_post( $event );
		}

		return false;
	}

	/**
	 * Get attendees by id and associated post type
	 * or default to using $post_id
	 *
	 * @param     $post_id
	 * @param null $post_type
	 *
	 * @return array|mixed
	 */
	public function get_attendees_by_id( $post_id, $post_type = null ) {
		if ( ! $post_type ) {
			$post_type = get_post_type( $post_id );
		}

		switch ( $post_type ) {
			case $this->ticket_object:
				$attendees = $this->get_attendees_by_product_id( $post_id );

				break;
			case self::ATTENDEE_OBJECT:
				$attendees = $this->get_all_attendees_by_attendee_id( $post_id );

				break;
			case $this->order_object:
			case $this->order_object_hpos:
				$attendees = $this->get_attendees_by_order_id( $post_id );

				break;
			default:
				$attendees = $this->get_attendees_by_post_id( $post_id );

				break;
		}

		/**
		 * Filters the attendees returned after a query.
		 *
		 * @since 4.7
		 *
		 * @param array  $attendees
		 * @param int    $post_id The post ID attendees were requested for.
		 * @param string $post_type
		 */
		return apply_filters( 'tribe_tickets_plus_woo_get_attendees', $attendees, $post_id, $post_type );
	}

	/**
	 * {@inheritdoc}
	 */
	public function get_attendee( $attendee, $post_id = 0 ) {
		if ( is_numeric( $attendee ) ) {
			$attendee = get_post( $attendee );
		}

		if (
			! $attendee instanceof WP_Post
			|| self::ATTENDEE_OBJECT !== $attendee->post_type
		) {
			return false;
		}

		$order_id      = get_post_meta( $attendee->ID, self::ATTENDEE_ORDER_KEY, true );
		$order_item_id = get_post_meta( $attendee->ID, $this->attendee_order_item_key, true );
		$checkin       = get_post_meta( $attendee->ID, $this->checkin_key, true );
		$optout        = get_post_meta( $attendee->ID, $this->attendee_optout_key, true );
		$security      = get_post_meta( $attendee->ID, $this->security_code, true );
		$product_id    = get_post_meta( $attendee->ID, $this->attendee_product_key, true );
		$user_id       = get_post_meta( $attendee->ID, $this->attendee_user_id, true );
		$ticket_sent   = (int) get_post_meta( $attendee->ID, $this->attendee_ticket_sent, true );

		$optout = filter_var( $optout, FILTER_VALIDATE_BOOLEAN );

		if ( empty( $product_id ) ) {
			return false;
		}

		$product          = get_post( $product_id );
		$product_title    = ( ! empty( $product ) ) ? $product->post_title : get_post_meta( $attendee->ID, $this->deleted_product, true ) . ' ' . __( '(deleted)', 'wootickets' );
		$ticket_unique_id = get_post_meta( $attendee->ID, '_unique_id', true );
		$ticket_unique_id = $ticket_unique_id === '' ? $attendee->ID : $ticket_unique_id;

		$meta = '';
		if ( class_exists( 'Tribe__Tickets_Plus__Meta' ) ) {
			$meta = get_post_meta( $attendee->ID, Tribe__Tickets_Plus__Meta::META_KEY, true );

			// Process Meta to include value, slug, and label
			if ( ! empty( $meta ) ) {
				$meta = $this->process_attendee_meta( $product_id, $meta );
			}
		}

		$order        = wc_get_order( $order_id );
		$order_id_url = '';
		if ( $order && $order instanceof WC_Order ) {
			$order_id_url = $order->get_edit_order_url();
		}

		// Add the Attendee Data to the Order data.
		$attendee_data = array_merge(
			$this->get_order_data( $order ),
			[
				'ticket'        => $product_title,
				'attendee_id'   => $attendee->ID,
				'order_item_id' => $order_item_id,
				'security'      => $security,
				'product_id'    => $product_id,
				'check_in'      => $checkin,
				'optout'        => $optout,
				'user_id'       => $user_id,
				'ticket_sent'   => $ticket_sent,
				'order_id_url'  => $order_id_url,

				// Fields for Email Tickets.
				'event_id'      => get_post_meta( $attendee->ID, self::ATTENDEE_EVENT_KEY, true ),
				'ticket_name'   => ! empty( $product ) ? $product->post_title : false,
				'holder_name'   => $this->get_holder_name( $attendee, $order ),
				'holder_email'  => $this->get_holder_email( $attendee, $order ),
				'ticket_id'     => $ticket_unique_id,
				'qr_ticket_id'  => $attendee->ID,
				'security_code' => $security,

				// Attendee Meta.
				'attendee_meta' => $meta,

				// Handle initial Attendee flags.
				'is_subscribed' => tribe_is_truthy( get_post_meta( $attendee->ID, $this->attendee_subscribed, true ) ),
				'is_purchaser'  => true,
			]
		);

		$attendee_data['is_purchaser'] = $attendee_data['holder_email'] === $attendee_data['purchaser_email'];

		/**
		 * Allow filtering the attendee information to return.
		 *
		 * @since 4.7
		 *
		 * @param array   $attendee_data The attendee information.
		 * @param string  $provider_slug The provider slug.
		 * @param WP_Post $attendee      The attendee post object.
		 * @param int     $post_id       The post ID of the attendee ID.
		 */
		return apply_filters( 'tribe_tickets_attendee_data', $attendee_data, $this->orm_provider, $attendee, $post_id );
	}

	/**
	 * Get Holder name from existing meta, if possible.
	 *
	 * @since 4.9
	 * @since 5.1.0 Added support for full name meta value.
	 *
	 * @param WP_Post  $attendee The attendee post object.
	 * @param WC_Order $order    The WooCommerce order object.
	 *
	 * @return string|null The holder name or null if not set.
	 */
	protected function get_holder_name( $attendee, $order ) {
		$holder_name = get_post_meta( $attendee->ID, $this->full_name, true );

		if ( 0 < strlen( $holder_name ) ) {
			return $holder_name;
		}

		if ( ! $order ) {
			return null;
		}

		$first_name = $order->get_billing_first_name();
		$last_name  = $order->get_billing_last_name();

		return $first_name . ' ' . $last_name;
	}

	/**
	 * Get Holder email from existing meta, if possible.
	 *
	 * @since 5.1.0
	 *
	 * @param WP_Post  $attendee The attendee post object.
	 * @param WC_Order $order    The WooCommerce order object.
	 *
	 * @return string|null The holder email or null if not set.
	 */
	protected function get_holder_email( $attendee, $order ) {
		$holder_email = get_post_meta( $attendee->ID, $this->email, true );

		if ( 0 < strlen( $holder_email ) ) {
			return $holder_email;
		}

		if ( ! $order ) {
			return null;
		}

		return $order->get_billing_email();
	}

	/**
	 * Retreive only order related information
	 *
	 *     order_id
	 *     order_id_display
	 *     order_id_link
	 *     order_id_link_src
	 *     order_status
	 *     order_status_label
	 *     order_warning
	 *     purchaser_name
	 *     purchaser_email
	 *     provider
	 *     provider_slug
	 *
	 * @param int|WC_Order $order_id The WooCommerce order ID or object.
	 *
	 * @return array
	 */
	public function get_order_data( $order_id ) {
		$order = null;

		if ( $order_id instanceof WC_Order ) {
			$order    = $order_id;
			$order_id = $order->get_id();
		} elseif ( is_numeric( $order_id ) ) {
			$order = wc_get_order( $order_id );
		}

		// The order does not exist so return some default values.
		if ( empty( $order ) || ! $order_id ) {
			return [
				'order_id'           => null,
				'order_id_display'   => null,
				'order_id_link'      => null,
				'order_id_link_src'  => null,
				'order_status'       => null,
				'order_status_label' => null,
				'order_warning'      => null,
				'purchaser_name'     => null,
				'purchaser_email'    => null,
				'provider'           => __CLASS__,
				'provider_slug'      => $this->orm_provider,
				'purchase_time'      => null,
			];
		}

		$order_id           = $order->get_id();
		$name               = $order->get_billing_first_name() . ' ' . $order->get_billing_last_name();
		$email              = $order->get_billing_email();
		$status             = $order->get_status();
		$order_status       = 'wc-' === substr( $status, 0, 3 ) ? substr( $status, 3 ) : $status;
		$order_status_label = wc_get_order_status_name( $order_status );
		$order_warning      = false;

		// Warning flag for refunded, cancelled and failed orders
		$warning_statues = tribe( 'tickets.status' )->get_statuses_by_action( 'warning', 'woo' );
		if ( in_array( $status, $warning_statues, true ) ) {
			$order_warning = true;
		}

		// Warning flag where the order post was trashed
		if ( ! empty( $order_status ) && 'trash' === $status ) {
			$order_status_label = sprintf( __( 'In trash (was %s)', 'event-tickets-plus' ), $order_status_label );
			$order_warning      = true;
		}

		// Warning flag where the order has been completely deleted
		if ( empty( $order_status ) && ! get_post( $order_id ) ) {
			$order_status_label = __( 'Deleted', 'event-tickets-plus' );
			$order_warning      = true;
		}

		$display_order_id = $order->get_order_number();

		$order_link_src = esc_url( get_edit_post_link( $order_id, true ) ? get_edit_post_link( $order_id, true ) : '' );
		$order_link       = sprintf( '<a class="row-title" href="%s">%s</a>', $order_link_src, esc_html( $display_order_id ) );


		$data = [
			'order_id'           => $order_id,
			'order_id_display'   => $display_order_id,
			'order_id_link'      => $order_link,
			'order_id_link_src'  => $order_link_src,
			'order_status'       => $order_status,
			'order_status_label' => $order_status_label,
			'order_warning'      => $order_warning,
			'purchaser_name'     => $name,
			'purchaser_email'    => $email,
			'provider'           => __CLASS__,
			'provider_slug'      => $this->orm_provider,
			'purchase_time'      => get_post_time( Tribe__Date_Utils::DBDATETIMEFORMAT, false, $order_id ),
		];

		/**
		 * Allow users to filter the Order Data
		 *
		 * @param array An associative array with the Information of the Order
		 * @param string What Provider is been used
		 * @param int Order ID
		 *
		 */
		$data = apply_filters( 'tribe_tickets_order_data', $data, $data['provider_slug'], $order_id );

		return $data;
	}

	/**
	 * Returns the order status.
	 *
	 * @todo remove safety check against existence of wc_get_order_status_name() in future release
	 *       (exists for backward compatibility with versions of WC below 2.2)
	 *
	 * @param int $order_id The order ID.
	 *
	 * @return string
	 */
	protected function order_status( $order_id ) {
		if ( ! function_exists( 'wc_get_order_status_name' ) ) {
			return __( 'Unknown', 'event-tickets-plus' );
		}

		return wc_get_order_status_name( get_post_status( $order_id ) );
	}

	/**
	 * Marks an attendee as checked in for an event
	 *
	 * @since 3.1.2
	 * @since 5.9.2 Add the `tec_tickets_attendee_checkin` filter to override the checkin process. Add the `$event_id`
	 *        parameter to the method signature.
	 * @since - Add the optional `$details` parameter to be able to set the checkin time and device_id.
	 *
	 * @param int                 $attendee_id The ID of the attendee that's being checked in.
	 * @param bool                $qr          True if from QR checkin process.
	 * @param int|null            $event_id    The ID of the ticket-able post the Attendee is being checked into.
	 * @param array<string|mixed> $details     Check-out details including timestamp and device_id information.
	 *
	 * @return bool Whether the Attendee was checked in or not.
	 */
	public function checkin( $attendee_id, $qr = false, $event_id = null, $details = [] ) {
		$checkin  = null;
		$attendee = $this->get_attendee( $attendee_id );
		$order    = wc_get_order( $attendee['order_id'] );

		$check_in_stati = apply_filters( 'event_tickets_attendees_woo_checkin_stati', [], $this->orm_provider, $attendee['order_id'] );
		if (
			$qr
			&& $order
			&& ! in_array( $order->get_status(), $check_in_stati, true )
		) {
			$checkin = false;
		}

		/**
		 * Allows filtering the Attendee check-in action before the default logic does it.
		 * Returning a non-null value from this filter will prevent the default logic from running.
		 *
		 * @since 5.9.2
		 *
		 * @param bool|null $checkin     Whether the Attendee should be checked in.
		 * @param int       $attendee_id The post ID of the Attendee being checked-in.
		 * @param int|null  $event_id    The ID of the ticket-able post the Attendee is being checked into.
		 * @param bool      $qr          Whether the check-in is being performed via QR code.
		 */
		$checkin = apply_filters( 'tec_tickets_attendee_checkin', $checkin, (int) $attendee_id, (int) $event_id, (bool) $qr );

		if ( $checkin !== null ) {
			return (bool) $checkin;
		}

		update_post_meta( $attendee_id, $this->checkin_key, 1 );

		if ( func_num_args() > 1 && $qr = func_get_arg( 1 ) ) {
			update_post_meta( $attendee_id, '_tribe_qr_status', 1 );
		}

		parent::save_checkin_details( $attendee_id, $qr, $details );

		/**
		 * Fires a checkin action
		 *
		 * @deprecated 4.7 Use event_tickets_checkin instead
		 *
		 * @param int       $attendee_id
		 * @param bool|null $qr
		 */
		do_action( 'wootickets_checkin', $attendee_id, $qr );

		return true;
	}

	/**
	 * Remove the Post Transients when a WooCommerce Ticket is Checked In
	 *
	 * @since 4.8.0
	 *
	 * @param int $attendee_id The attendee ID.
	 *
	 * @return void
	 */
	public function purge_attendees_transient( $attendee_id ) {
		$event_id = get_post_meta( $attendee_id, self::ATTENDEE_EVENT_KEY, true );
		if ( ! $event_id ) {
			return;
		}

		$current_transient = Tribe__Post_Transient::instance()->get( $event_id, Tribe__Tickets__Tickets::ATTENDEES_CACHE );
		if ( ! $current_transient ) {
			return;
		}

		Tribe__Post_Transient::instance()->delete( $event_id, Tribe__Tickets__Tickets::ATTENDEES_CACHE, $current_transient );

	}

	/**
	 * Add the extra options in the admin's new/edit ticket metabox
	 *
	 * @param int $post_id   The post ID.
	 * @param int $ticket_id The ticket ID.
	 *
	 * @return void
	 */
	public function do_metabox_capacity_options( $post_id, $ticket_id ) {
		/** @var Tribe__Tickets__Tickets_Handler $tickets_handler */
		$tickets_handler = tribe( 'tickets.handler' );

		$is_correct_provider = $tickets_handler->is_correct_provider( $post_id, $this );

		$url               = '';
		$stock             = '';
		$global_stock_mode = $tickets_handler->get_default_capacity_mode();
		$global_stock_cap  = 0;
		$capacity          = null;
		$event_capacity    = null;

		$stock_object = new Tribe__Tickets__Global_Stock( $post_id );

		if ( $stock_object->is_enabled() ) {
			$event_capacity = tribe_tickets_get_capacity( $post_id );
		}

		if ( ! empty( $ticket_id ) ) {
			$ticket              = $this->get_ticket( $post_id, $ticket_id );
			$is_correct_provider = $tickets_handler->is_correct_provider( $ticket_id, $this );

			if ( ! empty( $ticket ) ) {
				$stock             = $ticket->managing_stock() ? $ticket->stock() : '';
				$capacity          = tribe_tickets_get_capacity( $ticket->ID );
				$global_stock_mode = ( method_exists( $ticket, 'global_stock_mode' ) ) ? $ticket->global_stock_mode() : '';
				$global_stock_cap  = ( method_exists( $ticket, 'global_stock_cap' ) ) ? $ticket->global_stock_cap() : 0;
			}
		}

		// Bail when we are not dealing with this provider
		if ( ! $is_correct_provider ) {
			return;
		}

		include $this->plugin_path . 'src/admin-views/woocommerce-metabox-capacity.php';
	}

	/**
	 * Add the extra options in the admin's new/edit ticket metabox portion that is loaded via ajax
	 * Currently, that includes the sku, ecommerce links, and ticket history
	 *
	 * @since 4.6
	 * @since 5.6.9 Removed `tribe_is_frontend` so the SKU displays when using Community Tickets.
	 *
	 * @param int $post_id id of the event post
	 * @param int $ticket_id (null) id of the ticket
	 */
	public function do_metabox_advanced_options( $post_id, $ticket_id = null ) {
		$provider = __CLASS__;

		echo '<div id="' . sanitize_html_class( $provider ) . '_advanced" class="tribe-dependent" data-depends="#tec_tickets_ticket_provider" data-condition="' . esc_attr( $provider ) . '">';

		$this->do_metabox_sku_options( $post_id, $ticket_id );
		$this->do_metabox_ecommerce_links( $post_id, $ticket_id );

		/**
		 * Allows for the insertion of additional content into the ticket edit form - advanced section
		 *
		 * @since 4.6
		 *
		 * @param int Post ID
		 * @param string the provider class name
		 * @param int $ticket_id The ticket ID.
		 */
		do_action( 'tribe_events_tickets_metabox_edit_ajax_advanced', $post_id, $provider, $ticket_id );

		echo '</div>';
	}

	/**
	 * Add the sku field in the admin's new/edit ticket metabox
	 *
	 * @since 4.6
	 *
	 * @param int $post_id   The post ID.
	 * @param int $ticket_id The ticket ID.
	 *
	 * @return void
	 */
	public function do_metabox_sku_options( $post_id, $ticket_id = null ) {
		/** @var Tribe__Tickets__Tickets_Handler $tickets_handler */
		$tickets_handler = tribe( 'tickets.handler' );

		$sku                 = '';
		$is_correct_provider = $tickets_handler->is_correct_provider( $post_id, $this );

		if ( ! empty( $ticket_id ) ) {
			$ticket              = $this->get_ticket( $post_id, $ticket_id );
			$is_correct_provider = $tickets_handler->is_correct_provider( $ticket_id, $this );

			if ( ! empty( $ticket ) ) {
				$sku = get_post_meta( $ticket_id, '_sku', true );
			}
		}

		// Bail when we are not dealing with this provider
		if ( ! $is_correct_provider ) {
			return;
		}

		include $this->plugin_path . 'src/admin-views/woocommerce-metabox-sku.php';
	}

	/**
	 * Add the extra options in the admin's new/edit ticket metabox
	 *
	 * @since 4.6
	 *
	 * @param    $post_id int id of the event post
	 * @param int $ticket_id (null) id of the ticket
	 *
	 * @return void
	 */
	public function do_metabox_ecommerce_links( $post_id, $ticket_id = null ) {
		/** @var Tribe__Tickets__Tickets_Handler $tickets_handler */
		$tickets_handler = tribe( 'tickets.handler' );

		$is_correct_provider = $tickets_handler->is_correct_provider( $post_id, $this );

		if ( empty( $ticket_id ) ) {
			$ticket_id = tribe_get_request_var( 'ticket_id' );
		}

		$ticket              = $this->get_ticket( $post_id, $ticket_id );
		$is_correct_provider = $tickets_handler->is_correct_provider( $ticket_id, $this );

		// Bail when we are not dealing with this provider
		if ( ! $is_correct_provider ) {
			return;
		}

		include $this->plugin_path . 'src/admin-views/woocommerce-metabox-ecommerce.php';
	}

	/**
	 * Links to sales report for all tickets for this event.
	 *
	 * @since 5.9.0 Added $url_only parameter.
	 *
	 * @param int  $post_id The post id.
	 * @param bool $url_only Whether to return the URL only.
	 *
	 * @return string
	 */
	public function get_event_reports_link( $post_id, $url_only = false ) {
		$ticket_ids = (array) $this->get_tickets_ids( $post_id );
		if ( empty( $ticket_ids ) ) {
			return '';
		}

		$query = [
			'post_type' => 'tribe_events',
			'page'      => 'tickets-orders',
			'event_id'  => $post_id,
		];

		$report_url = add_query_arg( $query, admin_url( 'admin.php' ) );

		/**
		 * Filter the Event Ticket Orders (Sales) Report URL
		 *
		 * @param string Report URL
		 * @param int Event ID
		 * @param array Ticket IDs
		 *
		 * @return string
		 */
		$report_url = apply_filters( 'tribe_events_tickets_report_url', $report_url, $post_id, $ticket_ids );

		if ( $url_only ) {
			return $report_url;
		}

		return '<small> <a href="' . esc_url( $report_url ) . '">' . esc_html__( 'Event sales report', 'event-tickets-plus' ) . '</a> </small>';
	}

	/**
	 * Links to the sales report for this product.
	 * As of 4.6 we reversed the params and deprecated $event_id as it was never used
	 *
	 * @param deprecated $event_id_deprecated ID of the event post
	 * @param int        $ticket_id (null) id of the ticket
	 *
	 * @return string
	 */
	public function get_ticket_reports_link( $event_id_deprecated, $ticket_id ) {
		if ( ! empty( $event_id_deprecated ) ) {
			_deprecated_argument( __METHOD__, '4.6' );
		}

		if ( empty( $ticket_id ) ) {
			return '';
		}

		$query = [
			'page'        => 'wc-reports',
			'tab'         => 'orders',
			'report'      => 'sales_by_product',
			'product_ids' => $ticket_id,
		];

		return add_query_arg( $query, admin_url( 'admin.php' ) );
	}

	/**
	 * Registers a metabox in the WooCommerce product edit screen
	 * with a link back to the product related Event.
	 *
	 */
	public function woocommerce_meta_box() {
		$post_id = get_post_meta( get_the_ID(), $this->event_key, true );

		if ( ! empty( $post_id ) ) {
			add_meta_box( 'wootickets-linkback', 'Event', [ $this, 'woocommerce_meta_box_inside' ], 'product', 'normal', 'high' );
		}
	}

	/**
	 * Contents for the metabox in the WooCommerce product edit screen
	 * with a link back to the product related Event.
	 */
	public function woocommerce_meta_box_inside() {
		$post_id = get_post_meta( get_the_ID(), $this->event_key, true );
		if ( ! empty( $post_id ) ) {
			$text = esc_html(
				sprintf(
					// translators: %s is ticket.
					__( 'This is a %s for the event:', 'event-tickets-plus' ),
					tribe_get_ticket_label_singular_lowercase( 'woo_meta_box' )
				)
			);

			echo sprintf(
				'%s <a href="%s">%s</a>',
				$text,
				esc_url( get_edit_post_link( $post_id ) ),
				esc_html( get_the_title( $post_id ) )
			);
		}
	}

	/**
	 * Indicates if global stock support is enabled (for WooCommerce the default is
	 * true).
	 *
	 * @return bool
	 */
	public function supports_global_stock() {
		/**
		 * Allows the declaration of global stock support for WooCommerce tickets
		 * to be overridden.
		 *
		 * @param bool $enable_global_stock_support
		 */
		return (bool) apply_filters( 'tribe_tickets_woo_enable_global_stock', true );
	}

	/**
	 * Determine if the event is set to use global stock for its tickets.
	 *
	 * @param int $post_id
	 *
	 * @return bool
	 */
	public function uses_global_stock( $post_id ) {
		// In some cases (version mismatch with Event Tickets) the Global Stock class may not be available
		if ( ! class_exists( 'Tribe__Tickets__Global_Stock' ) ) {
			return false;
		}

		$global_stock = new Tribe__Tickets__Global_Stock( $post_id );

		return $global_stock->is_enabled();
	}

	/**
	 * Get's the WC product price html
	 *
	 * @param int|object $product
	 * @param array|boolean $attendee
	 *
	 * @return string
	 */
	public function get_price_html( $product, $attendee = false ) {
		if ( is_numeric( $product ) ) {
			if ( class_exists( 'WC_Product_Simple' ) ) {
				$product = new WC_Product_Simple( $product );
			} else {
				$product = new WC_Product( $product );
			}
		}

		if ( ! method_exists( $product, 'get_price_html' ) ) {
			return '';
		}

		$price_html = $product->get_price_html();

		/**
		 * Allow filtering of the Price HTML
		 *
		 * @since 4.3.2
		 *
		 * @param string $price_html
		 * @param mixed  $product
		 * @param mixed  $attendee
		 *
		 */
		return apply_filters( 'tribe_events_wootickets_ticket_price_html', $price_html, $product, $attendee );
	}

	/**
	 * Gets the product price value
	 *
	 * @since 4.6
	 *
	 * @param int|WP_Post $product The product.
	 *
	 * @return string
	 */
	public function get_price_value( $product ) {
		if ( ! $product instanceof WP_Post ) {
			$product = get_post( $product );
		}

		if ( ! $product instanceof WP_Post ) {
			return false;
		}

		$product = wc_get_product( $product->ID );

		return $product->get_price();
	}

	/**
	 * Adds an action to resend the tickets to the customer
	 * in the WooCommerce actions dropdown, in the order edit screen.
	 *
	 * @since 5.9.1 Updated logic to new WooCommerce HPOS requirement.
	 *
	 * @param array $emails The emails.
	 *
	 * @return array
	 */
	public function add_resend_tickets_action( $emails ) {
		$order = wc_get_order( get_the_ID() );

		if ( empty( $order ) ) {
			return $emails;
		}

		$has_tickets = $order->get_meta( $this->order_has_tickets, true );

		if ( ! $has_tickets ) {
			return $emails;
		}

		if ( version_compare( wc()->version, '3.2.0', '>=' ) ) {
			$emails['resend_tickets_email'] = esc_html__( 'Resend tickets email', 'event-tickets-plus' );
		} else {
			$emails[] = 'wootickets';
		}

		return $emails;
	}

	/**
	 * (Re-)sends the tickets email on request.
	 *
	 * Accepts either the order ID or the order object itself.
	 *
	 * @since 4.5.6
	 *
	 * @param WC_Order|int $order_ref
	 **/
	public function send_tickets_email( $order ) {
		// Ensure $order_ref is a WC_Order object.
		if ( ! $order instanceof WC_Order ) {
			$order = wc_get_order( $order );
		}

		// Once we normalize we need it to be a WC_Order.
		if ( ! $order instanceof WC_Order ) {
			return;
		}

		$order_id = $order->get_id();
		$order->update_meta_data(
			$this->mail_sent_meta_key,
			'1'
		);
		$order->save_meta_data();

		// Ensure WC_Emails exists else our attempt to mail out tickets will fail.
		WC_Emails::instance();

		/**
		 * Fires when a ticket order is complete.
		 *
		 * Back-compatibility action hook.
		 *
		 * @since 4.1
		 *
		 * @param int $order_id The order post ID for the ticket.
		 */
		do_action(
			'wootickets-send-tickets-email',
			$order_id
		);
	}

	/**
	 * Send RSVPs/tickets email for attendees.
	 *
	 * @since 5.1.0
	 *
	 * @param array $attendees List of attendees.
	 * @param array $args      {
	 *      The list of arguments to use for sending ticket emails.
	 *
	 *      @type string       $subject     The email subject.
	 *      @type string       $content     The email content.
	 *      @type string       $from_name   The name to send tickets from.
	 *      @type string       $from_email  The email to send tickets from.
	 *      @type array|string $headers     The list of headers to send.
	 *      @type array        $attachments The list of attachments to send.
	 *      @type string       $provider    The provider slug (rsvp, tpp, woo, edd).
	 *      @type int          $post_id     The post/event ID to send the emails for.
	 *      @type string|int   $order_id    The order ID to send the emails for.
	 * }
	 *
	 * @return int The number of emails sent successfully.
	 */
	public function send_tickets_email_for_attendees( $attendees, $args = [] ) {
		$args = array_merge(
			[
				'provider' => 'woo',
			],
			$args
		);

		return parent::send_tickets_email_for_attendees( $attendees, $args );
	}

	private function get_cancelled( $ticket_id ) {
		$cancelled = Tribe__Tickets_Plus__Commerce__WooCommerce__Orders__Cancelled::for_ticket( $ticket_id );

		return $cancelled->get_count();
	}

	/*
	 * Get the number of refunded orders for a ticket.
	 * Works with partial and full refunds.
	 *
	 * @since 4.7.3
	 *
	 * @param int $ticket_id Ticket ID
	 *
	 * @return int
	 */
	private function get_refunded( $ticket_id ) {
		/** @var Tribe__Tickets_Plus__Commerce__WooCommerce__Orders__Refunded $refunded */
		$refunded = tribe( 'commerce.woo.order.refunded' );

		return $refunded->get_count( $ticket_id );
	}

	/**
	 * Completes the order.
	 *
	 * @param int $order_id The order ID.
	 *
	 * @return void
	 */
	protected function complete_order( $order_id ) {
		$this->send_tickets_email( $order_id );

		// Clear WooCommerce Cart once the order is Done
		if ( null !== WC()->cart ) {
			WC()->cart->empty_cart();
		}

		/**
		 * Fires when a ticket order is complete.
		 *
		 * @since 4.2
		 *
		 * @param int $order_id The order post ID for the ticket.
		 */
		do_action( 'event_tickets_woo_complete_order', $order_id );
	}

	/**
	 * Filter the maximum quantity allowed to purchase at a time for WooCommerce.
	 *
	 * @since 4.8.1
	 * @since 4.11.1 If backorders are not allowed and Stock Quantity is lower than zero, correct it to be zero.
	 * @since 4.12.0 Adjust return value to match how Event Tickets' filter changed (now requires a value of 1+,
	 *               even if Unlimited).
	 *
	 * @param int                           $available_at_a_time Max quantity allowed to be purchased at a time (0+).
	 * @param Tribe__Tickets__Ticket_Object $ticket              Ticket Object.
	 *
	 * @return int Zero is out of stock, else it's limited by ticket stock or max allowed to purchase in a single
	 *             action (always a positive integer, even if Unlimited).
	 */
	public function filter_ticket_max_purchase( $available_at_a_time, $ticket ) {
		// Bails on invalid Ticket ID.
		if (
			empty( $ticket->ID )
			|| ! is_numeric( $ticket->ID )
		) {
			return $available_at_a_time;
		}

		if ( 'Tribe__Tickets_Plus__Commerce__WooCommerce__Main' !== $ticket->provider_class ) {
			return $available_at_a_time;
		}

		if ( class_exists( 'WC_Product_Simple' ) ) {
			$product = new WC_Product_Simple( $ticket->ID );
		} else {
			$product = new WC_Product( $ticket->ID );
		}

		/**
		 * Protect against negative stock quantity.
		 * If backorders are enabled or `null`, will be corrected to `-1` for Unlimited.
		 */
		$stock_qty = $product->get_stock_quantity();

		if ( ! is_numeric( $stock_qty ) ) {
			$stock_qty = - 1;
		} elseif ( 0 > (int) $stock_qty ) {
			// Don't confuse a Stock Qty of -1 as being Unlimited
			$stock_qty = 0;
		}

		/**
		 * Max Quantity will be unlimited if backorders are allowed, restricted to 1 if the product is constrained to
		 * be sold individually, or else set to the available stock quantity.
		 */
		$max_quantity = $product->backorders_allowed() ? - 1 : $stock_qty;
		$max_quantity = $product->is_sold_individually() ? 1 : $max_quantity;

		// If Unlimited, set to Max At A Time.
		if ( - 1 === $max_quantity ) {
			$max_quantity = $available_at_a_time;
		}

		// Quantity in stock may be less than Max At A Time.
		$max_quantity = min( $available_at_a_time, $max_quantity );

		return $max_quantity;
	}

	/**
	 * Excludes WooCommerce product post types from the list of supported post types that Tickets can be attached to
	 *
	 * @since 4.0.5
	 *
	 * @param array $post_types Array of supported post types
	 *
	 * @return array
	 */
	public function exclude_product_post_type( $post_types ) {
		if ( isset( $post_types['product'] ) ) {
			unset( $post_types['product'] );
		}

		return $post_types;
	}

	/**
	 * Returns the ticket price taking the context of the request into account.
	 *
	 * @since 5.7.4 Make use of `wc_get_price_to_display` to fetch correct price.
	 *
	 * @param WC_Product $product
	 *
	 * @return string
	 */
	protected function get_price_value_for( $product ) {
		return $this->should_show_regular_price() ? $product->get_regular_price() : wc_get_price_to_display( $product );
	}

	/**
	 * Maybe format price display for ticket edit form.
	 *
	 * @since 5.2.7
	 *
	 * @param string $price Price.
	 *
	 * @return string Formatted price.
	 */
	public function get_formatted_price( $price ) {

		if ( ! $this->should_show_regular_price() ) {
			return $price;
		}

		$args = [
			'decimal_separator'  => wc_get_price_decimal_separator(),
			'thousand_separator' => wc_get_price_thousand_separator(),
			'decimals'           => wc_get_price_decimals(),
		];

		$formatted_price = number_format( (float) $price, $args['decimals'], $args['decimal_separator'], $args['thousand_separator'] );

		/**
		 * Filter the formatted price for ticket edit form.
		 *
		 * @since 5.2.7
		 *
		 * @param string $formatted_price Formatted price.
		 * @param string $price Original price data.
		 * @param array $args Arguments containing the formatting options for numbers of decimals, decimals and thousands separators.
		 */
		return apply_filters( 'tribe_tickets_plus_ticket_edit_form_formatted_price', $formatted_price, $price, $args );
	}

	/**
	 * @return bool
	 */
	protected function should_show_regular_price() {
		$screen = function_exists( 'get_current_screen' ) ? get_current_screen() : null;

		return doing_action( 'wp_ajax_tribe-ticket-edit-' . __CLASS__ )
			|| doing_action( 'wp_ajax_tribe-ticket-add-' . __CLASS__ )
			|| doing_action( 'wp_ajax_tribe-ticket-delete-' . __CLASS__ )
			|| 'tribe-ticket-edit' === tribe_get_request_var( 'action', '' )
			|| tribe_get_request_var( 'is_admin', false )
			|| ( is_admin()
					&& ! empty( $screen )
					&& 'post' === $screen->base
					&& 'edit' === $screen->parent_base );
	}

	/**
	 * @param WC_Product $product
	 *
	 * @return mixed
	 */
	protected function get_regular_price_html( $product ) {
		$this->product = $product;

		// The hook names are slightly different in WC 3.x vs WC 2.x
		$filter_prefix = 'woocommerce';

		if ( version_compare( WC()->version, '3.0', '>=' ) ) {
			$filter_prefix .= '_product';
		}

		add_filter( "{$filter_prefix}_get_price", [ $this, 'get_regular_price' ], 99, 2 );

		$price_html = $product->get_price_html();

		remove_filter( "{$filter_prefix}_get_price", [ $this, 'get_regular_price' ], 99 );

		return $price_html;
	}

	/**
	 * @param mixed      $price
	 * @param WC_Product $product
	 *
	 * @return string
	 */
	public function get_regular_price( $price, $product ) {
		if ( ! $product instanceof WC_Product ) {
			return $price;
		}

		if ( $this->get_product_id( $product ) == $this->get_product_id( $this->product ) ) {
			return $product->get_regular_price();
		}

		return $price;
	}


	/**
	 * Renders the tabbed view header before the report.
	 *
	 * @param Tribe__Tickets__Attendees $handler
	 */
	public function render_tabbed_view( Tribe__Tickets__Attendees $handler ) {
		$post = $handler->get_post();

		$has_tickets = count( (array) self::get_tickets( $post->ID ) );
		if ( ! $has_tickets ) {
			return;
		}

		add_filter( 'tribe_tickets_attendees_show_title', '__return_false' );

		$tabbed_view = new Tribe__Tickets_Plus__Commerce__WooCommerce__Tabbed_View__Report_Tabbed_View();
		$tabbed_view->register();
	}

	/**
	 * Given a WooCommerce product object, returns the product ID.
	 *
	 * This helper allows us to support both WooCommerce 2.x and 3.x, which allow access to
	 * the product ID in slightly different ways.
	 *
	 * @since 5.9.1 Added check to ensure $product is an object.
	 *
	 * @param WC_Data|WC_Product $product
	 *
	 * @return int
	 */
	public function get_product_id( $product ) {
		// Ensure $product is an object.
		if ( ! is_object( $product ) ) {
			return 0;
		}

		return method_exists( $product, 'get_id' )
			? (int) $product->get_id()
			: (int) $product->id;
	}

	/**
	 * Given an order and an order item, returns the product associated with the item.
	 *
	 * This helper allows us to support both WooCommerce 2.x and 3.x, which each have different
	 * ways of providing access to that information.
	 *
	 * @since 5.9.1 Added check to ensure $item is an object.
	 *
	 * @param WC_Order      $order
	 * @param WC_Order_Item $item
	 *
	 * @return WC_Product
	 */
	public function get_product_from_item( $order, $item ) {
		// Ensure $item is an object.
		if ( ! is_object( $item ) ) {
			return null;
		}

		return method_exists( $item, 'get_product' )
			? $item->get_product()
			: $order->get_product_from_item( $item );
	}

	/**
	 * If a user saves a ticket in their cart and after a few hours / days the ticket is still on the cart but the ticket has
	 * expired or is no longer available for sales the item on the cart shouldn't be processed.
	 *
	 * Instead of removing the product from the cart we send a notice and avoid to checkout so the user knows exactly why can
	 * move forward and he needs to take an action before doing so.
	 *
	 * @since 4.7.3
	 *
	 * @return bool
	 */
	public function validate_tickets() {

		foreach ( WC()->cart->get_cart() as $cart_item_key => $values ) {
			$product_id = empty( $values['product_id'] ) ? null : $values['product_id'];

			if ( is_null( $product_id ) ) {
				continue;
			}

			$ticket_type = Tribe__Tickets__Tickets::load_ticket_object( $product_id );

			if ( ! $ticket_type || ! $ticket_type instanceof Tribe__Tickets__Ticket_Object ) {
				continue;
			}

			if ( ! $ticket_type->date_in_range() ) {

				$message = sprintf(
					__( 'The ticket: %1$s, in your cart is no longer available or valid. You need to remove it from your cart in order to continue.', 'event-tickets-plus' ),
					$ticket_type->name
				);

				wc_add_notice( $message, 'error' );

				return false;
			}
		}

		return true;
	}

	/**
	 * Redirect to the cart from a POST type of request to a request with code 303 in order to prevent the browser
	 * to send the same data multiple times on browser refresh.
	 *
	 * @see https://en.wikipedia.org/wiki/Post/Redirect/Get
	 *
	 * @since 4.7.3
	 */
	public function redirect_to_cart() {
		$cart_url = get_transient( $this->get_cart_transient_key() );

		if ( ! empty( $cart_url ) ) {

			/**
			 * Filter to allow the change the URL where the users are redirected by default uses the wc_get_cart_url()
			 * value.
			 *
			 * @since 4.7.3
			 *
			 * @param string $location
			 */
			$location = apply_filters( 'tribe_tickets_plus_woo_cart_location', $cart_url );

			delete_transient( $this->get_cart_transient_key() );

			wp_redirect( $location, WP_Http::SEE_OTHER );
			die();
		}
	}

	/**
	 * Get the key used to store the cart transient URL.
	 *
	 * @since 4.7.3
	 *
	 * @return string
	 */
	public function get_cart_transient_key() {
		return $this->cart_location_cache_prefix . $this->get_session_hash();
	}

	/**
	 * Generates as hash based on the user session, user cart or user ID
	 *
	 * @since 4.7.3
	 *
	 * @return string
	 */
	private function get_session_hash() {

		$hash = get_current_user_id();

		if ( defined( 'COOKIEHASH' ) && isset( $_COOKIE[ 'wp_woocommerce_session_' . COOKIEHASH ] ) ) {
			$hash = $_COOKIE[ 'wp_woocommerce_session_' . COOKIEHASH ];
		} elseif ( ! empty( $_COOKIE['woocommerce_cart_hash'] ) ) {
			$hash = $_COOKIE['woocommerce_cart_hash'] . get_current_user_id();
		}

		return md5( $hash );
	}

	/**
	 * Get the default Currency selected for Woo
	 *
	 * @since 4.7.3
	 *
	 * @return string
	 */
	public function get_currency() {
		return function_exists( 'get_woocommerce_currency' ) ? get_woocommerce_currency() : parent::get_currency();
	}

	/**
	 * Clean the attendees cache every time an order changes its
	 * status, so the changes are reflected instantly.
	 *
	 * @since 4.7.3
	 *
	 * @param int $order_id
	 */
	public function reset_attendees_cache( $order_id ) {

		// Get the items purchased in this order
		$order       = wc_get_order( $order_id );
		$order_items = $order->get_items();

		// Bail if the order is empty.
		if ( empty( $order ) ) {
			return;
		}

		/** @var Tribe__Post_Transient $transient */
		$ticket_ids = [];
		$transient  = tribe( 'post-transient' );

		// Iterate over each product
		foreach ( (array) $order_items as $item_id => $item ) {
			$product_id = isset( $item['product_id'] ) ? $item['product_id'] : $item['id'];
			// Get the event this tickets is for
			$post_id = get_post_meta( $product_id, $this->event_key, true );

			/**
			 * Action fired when an attendee data is updated when on the cache.
			 *
			 * @since 4.10.1.2
			 *
			 * @param int $post_id ID of the event associated with the order.
			 * @param int $order_id ID of the order attached to this event.
			 * @param array $item Details of the order
			 */
			do_action( 'tribe_tickets_plus_woo_reset_attendee_cache', $post_id, $order_id, $item );

			if ( ! empty( $post_id ) ) {
				$ticket_ids[] = $product_id;

				// Delete the attendees cache for that event
				$transient->delete( $post_id, Tribe__Tickets__Tickets::ATTENDEES_CACHE );
			}
		}

		$cache_key = self::ORDER_COUNT_CACHE_KEY . '_incomplete';

		// Delete count transients for tickets in the order.
		foreach ( $ticket_ids as $ticket_id ) {
			$transient->delete( $ticket_id, $cache_key );
		}
	}

	/**
	 * Sincronize the Event cost from an array of
	 * product IDs
	 *
	 * @since 4.7.3
	 *
	 * @param array $product_ids
	 */
	public function syncronize_products( $product_ids ) {

		if ( $product_ids ) {

			foreach ( $product_ids as $product_id ) {
				$event = $this->get_event_for_ticket( $product_id );

				// This product is not connected with an event
				if ( ! $event ) {
					continue;
				}

				// Trigger an update
				Tribe__Events__API::update_event_cost( $event->ID );
			}
		}
	}

	/**
	 * Get the warning tooltip HTML for the ticket table
	 *
	 * @param Tribe__Tickets__Ticket_Object|int $ticket ticket object (or ID)
	 * @param int $event_id event ID for the ticket
	 *
	 * @return string HTML string for tooltip insertion
	 */
	public function get_ticket_table_warnings( $ticket, $event_id ) {
		// no ticket, no event? bail
		if ( empty( $ticket ) || empty( $event_id ) ) {
			return;
		}

		// Just in case...
		if ( is_numeric( $ticket ) ) {
			$ticket = $this->get_ticket( $event_id, $ticket );
		}

		if ( __CLASS__ !== $ticket->provider_class ) {
			return;
		}

		$messages  = [];
		$inventory = (int) $ticket->inventory();
		$stock     = (int) $ticket->stock();
		$product   = wc_get_product( $ticket->ID );

		/** @var Tribe__Tickets__Attendees $tickets_attendees */
		$tickets_attendees = tribe( 'tickets.attendees' );

		if ( -1 !== $ticket->capacity() ) {
			$shared_stock = new Tribe__Tickets__Global_Stock( $event_id );

			if (
				$inventory !== $stock
				&& ( ! $shared_stock->is_enabled() || $stock < (int) $shared_stock->get_stock_level() )
			) {
				$messages['mismatch'] = sprintf(
					_x( 'The number of Complete ticket sales does not match the number of attendees. Please check the %1$sAttendees list%2$s and adjust ticket stock in WooCommerce as needed.', 'event-tickets-plus' ),
					'<a href="' . $tickets_attendees->get_report_link( get_post( $event_id ) ) . '">',
					'</a>'
				);
			}
		}

		if ( 'own' === $ticket->global_stock_mode() && ! $product->get_manage_stock() ) {
			$messages['stock'] = sprintf(
				_x( '"Unlimited" will be displayed unless you enable the WooCommerce\'s "Manage stock" setting. You can do so %1$shere%2$s.', 'event-tickets-plus' ),
				'<a href="' . esc_url( $ticket->admin_link ) . '">',
				'</a>'
			);
		}

		if ( empty( $messages ) ) {
			return;
		}

		ob_start();
		?>
		<div class="tribe-tooltip" aria-expanded="false">
			<span class="dashicons dashicons-warning required"></span>
			<div class="down" <?php if ( 1 < count( $messages ) ) { echo 'style="width: 370px;"';} ?>>
				<?php foreach( $messages as $type => $message ) : ?>
					<p>
						<span><?php echo $message; ?></span>
					</p>
				<?php endforeach;?>
			</div>
		</div>
		<?php

		echo ob_get_clean();
	}

	/**
	 * Add our class suffix to the list of classes for the attendee registration form
	 * This gets appended to `tribe-tickets__item__attendee__fields__form--` or `tribe-tickets__attendee-tickets-form--`
	 * so keep it short and sweet
	 *
	 * @param array $classes existing array of classes
	 * @return array $classes with our class added
	 */
	public function tribe_attendee_registration_form_class( $classes ) {
		$classes[ self::ATTENDEE_OBJECT ] = 'woo';

		return $classes;
	}

	/**
	 * Filter the provider object to return this class if tickets are for this provider.
	 *
	 * @since 4.11.0
	 *
	 * @param object $provider_obj
	 * @param string $provider
	 *
	 * @return object
	 */
	function tribe_attendee_registration_cart_provider( $provider_obj, $provider ) {
		$options = [
			'woo',
			'tribe_wooticket',
			__CLASS__,
		];

		if ( in_array( $provider, $options, true ) ) {
			return $this;
		}

		return $provider_obj;
	}

	/**
	 * Output notice explaining to user that they're at Attendee Registration page instead of Checkout because ticket
	 * quantity was modified in Cart.
	 *
	 * @since 4.11.0
	 *
	 * @param string $passed_provider       The 'provider' $_REQUEST var.
	 * @param string $passed_provider_class The class string or empty string if ticket provider is not found.
	 * @param array  $events                The array of events, which might be empty.
	 */
	public function woo_attendee_registration_notice_cart_qty_change( $passed_provider, $passed_provider_class, $events ) {
		if (
			$this->cart_change_notice_displayed
			|| self::ATTENDEE_OBJECT !== $passed_provider
			|| empty( $events )
			|| false === tribe_is_truthy( WC()->session->get( 'tribe_ar_ticket_updated' ) )
			|| ! tribe_is_truthy( tribe_get_option( 'ticket-attendee-modal', false ) )
		) {
			return;
		}

		/** @var Tribe__Tickets__Attendee_Registration__View $view */
		$view = tribe( 'tickets.attendee_registration.view' );
		?>
		<div class="tribe-common">
			<?php
			$view->template(
				'components/notice',
				[
					'id' => 'tribe-tickets-plus__ar-notice__cart-qty-change',
					'notice_classes' => [
						'tribe-tickets-plus-woo-ar-notice__cart-qty-change',
					],
					'title' => _x(
						'Updated Ticket Quantities',
						'Attendee Registration notice heading text when Woo cart quantity changed',
						'event-tickets-plus'
					),
					'content' => sprintf(
						esc_html_x(
							'You\'ve arrived here because you updated ticket quantities in the Cart. Please verify your attendee information, then click "Save Attendee Info" in order to proceed to Checkout.',
							'Attendee Registration notice paragraph text when Woo cart quantity changed',
							'event-tickets-plus'
						)
					)
				]
			);
			?>
		</div>
		<?php
		$this->cart_change_notice_displayed = true;
	}

	/**
	 * Clear the WC session var when we load checkout.
	 *
	 * @since 4.11.0
	 *
	 * @param mixed $unused_var An unused variable we don't need to reference from the hook.
	 *
	 * @return mixed The unused variable just as it was passed from the hook.
	 */
	public function clear_tribe_ar_ticket_updated( $unused_var = null ) {
		if ( ! function_exists( 'WC' ) ) {
			return $unused_var;
		}

		$wc = WC();

		if ( empty( $wc->session ) ) {
			return $unused_var;
		}

		$wc->session->__unset( 'tribe_ar_ticket_updated' );

		return $unused_var;
	}

	/**
	 * Adds a link back to the attendee registration page and cart from checkout.
	 * Adds provider param to links.
	 *
	 * @since 4.11.0
	 */
	public function add_checkout_links() {
		/** @var \Tribe__Tickets_Plus__Commerce__WooCommerce__Cart $cart*/
		$cart = tribe( 'tickets-plus.commerce.woo.cart' );

		$tickets_in_cart = $cart->get_tickets_in_cart();

		/** @var Tribe__Tickets_Plus__Meta $tickets_meta */
		$tickets_meta = tribe( 'tickets-plus.meta' );

		$cart_has_meta = $tickets_meta->cart_has_meta( $tickets_in_cart );

		echo '<div class="tribe-checkout-backlinks">';

		echo sprintf(
			'<a class="tribe-checkout-backlink" href="%1$s">%2$s</a>',
			esc_url( $cart->get_cart_url() ),
			esc_html__( 'Return to cart', 'event-tickets-plus' )
		);

		// Only show the AR link if we have ARI.
		if ( ! empty( $cart_has_meta ) ) {
			/** @var Tribe__Tickets__Attendee_Registration__Main $attendee_registration */
			$attendee_registration = tribe( 'tickets.attendee_registration' );
			$provider_slug = tribe_tickets_get_provider_query_slug();

			echo sprintf(
				'<a class="tribe-checkout-backlink" href="%1$s">%2$s</a>',
				esc_url( add_query_arg( $provider_slug, self::ATTENDEE_OBJECT, $attendee_registration->get_url() ) ),
				esc_html__( 'Edit attendee info', 'event-tickets-plus' )
			);
		}

		echo '</div>';
	}

	/**
	 * Get the price suffix from WooCommerce method.
	 *
	 * @param WC_Product $product the WooCommerce product.
	 * @param string     $price to calculate, left blank to just use get_price().
	 * @param integer    $qty   passed on to get_price_including_tax() or get_price_excluding_tax().
	 *
	 * @since 4.12.0
	 *
	 * @return string
	 */
	public function get_price_suffix( $product, $price = '', $qty = 1 ) {
		return $product->get_price_suffix( $price, $qty );
	}

	/**
	 * Temporarily filter the orders as queried to remove the invalid false values.
	 *
	 * Remove this after WooCommerce 5.1 is released which contains a fix for this.
	 *
	 * @since 5.2.0
	 *
	 * @param array<WC_Order|false> $orders List of order objects.
	 *
	 * @return array<WC_Order|false> List of order objects.
	 */
	public function temporarily_filter_order_query( $orders ) {
		// For some reason, $orders may not be an array. Some other plugin(s) may be using the filter incorrectly.
		if ( is_array( $orders ) ) {
			$orders = array_filter( $orders );
		}

		return $orders;
	}

	/**
	 * Add order note in WooCommerce order when an attendee is delete.
	 *
	 * @since 5.2.5
	 *
	 * @param int $post_id Post or Event ID.
	 * @param int $attendee_id Attendee ID for deleted attendee.
	 */
	public function update_order_note_for_deleted_attendee( $post_id, $attendee_id ) {
		if ( empty( $post_id ) || empty( $attendee_id ) ){
			return;
		}

		$attendee = $this->get_attendee( $attendee_id );

		if ( empty( $attendee ) ) {
			return;
		}

		$order = wc_get_order( $attendee['order_id'] );

		// Bail if the order is empty.
		if ( empty( $order ) ) {
			return;
		}

		// Translators: %1$s: Attendee post object ID, %2$s: Generated Ticket Serial.
		$order->add_order_note( sprintf( __( 'Attendee (%1$s) with Ticket #%2$s was deleted by admin.', 'event-tickets-plus' ), $attendee[ 'attendee_id' ], $attendee[ 'ticket_id' ] ) );
	}

	/**
	 * Check if given attendee should reduce stock or not.
	 *
	 * @since 5.2.5
	 *
	 * @param array $attendee Attendee data.
	 *
	 * @return bool
	 */
	public function attendee_decreases_inventory( array $attendee ) {

		$order_id = Tribe__Utils__Array::get( $attendee, 'order_id' );

		$order = wc_get_order( $order_id );

		// Bail if the order is empty, return true to decrease attendees.
		if ( empty( $order ) ) {
			return true;
		}

		$order_status = $order->get_status();
		// For cancelled orders inventory is restocked, so we should not decrease inventory for this case.
		if ( 'cancelled' === $order_status ) {
			return false;
		}

		// For refunded and trashed orders inventory is not restocked automatically, we should only decrease if the order was not restocked.
		if ( in_array( $order_status, [ 'refunded', 'trash' ], true ) && tribe_is_truthy( $order->get_meta( $this->restocked_refunded_order ) ) ) {
			// Don't count the attendee if the refunded order was restocked.
			return false;
		}

		return true;
	}

	/**
	 * Adds an action to restock the ticket items for any refunded orders.
	 *
	 * @since 5.2.5
	 * @since 5.10.2 Added the $order parameter.
	 * @deprecated 6.0.3 The $order parameter is no longer used.
	 *
	 * @param array    $actions List of actions.
	 * @param WC_Order $order (Deprecated) Order object.
	 *
	 * @return array
	 *
	 */
	public function add_restock_action_for_refunded_order( $actions, $order = null ) {
		if ( ! empty( $order ) ) {
			_deprecated_argument( __METHOD__, '6.0.3', 'The `$order` argument is deprecated and no longer used.' );
		}

		$order_id = get_the_ID();

		if ( empty( $order_id ) ) {
			return $actions;
		}

		$order = wc_get_order( $order_id );

		// Bail if the order is empty. Return $actions.
		if ( empty( $order ) ) {
			return $actions;
		}

		$has_tickets = tribe_is_truthy( $order->get_meta( $this->order_has_tickets ) );

		if ( ! $has_tickets ) {
			return $actions;
		}

		$restocked_already = tribe_is_truthy( $order->get_meta( $this->restocked_refunded_order ) );

		if ( $restocked_already ) {
			return $actions;
		}

		if ( 'refunded' != $order->get_status() ) {
			return $actions;
		}

		$actions['event_tickets_plus_restock_refunded_tickets'] = esc_html__( 'Restock Tickets from this order', 'event-tickets-plus' );

		return $actions;
	}

	/**
	 * Handle restock action for refund orders.
	 *
	 * @since 5.2.5
	 *
	 * @param WC_Order $order
	 */
	public function handle_restock_action_for_refunded_order( $order ) {

		if ( 'refunded' != $order->get_status() ) {
			return;
		}

		$restocked_already = tribe_is_truthy( $order->get_meta( $this->restocked_refunded_order ) );

		if ( $restocked_already ) {
			return;
		}

		// Increase the stock for both regular product and shared cap products.
		wc_increase_stock_levels( $order->get_id() );

		// Set meta to skip the item count while counting attendee stock.
		$order->add_meta_data( $this->restocked_refunded_order, true , true );

		// Update Order notes to keep a log for this restock action for admin.
		$order->add_order_note( __( 'Tickets from this order are re-stocked', 'event-tickets-plus' ) );

		// Save the order meta.
		$order->save();
	}

	/**
	 * Handles if email sending is allowed.
	 *
	 * @version 6.6.0
	 *
	 * @since 5.3.1
	 * @since 6.6.0 Add check if the attendee is an array.
	 *
	 * @param WP_Post|null $ticket                The ticket post object if available, otherwise null.
	 * @param array|null   $attendee              The attendee information if available, otherwise null.
	 *
	 * @return boolean
	 */
	public function allow_resending_email( $ticket = null, $attendee = null ) {
		if ( empty( $attendee ) || ! is_array( $attendee ) ) {
			// If the attendee is not an array, return false.
			$allow_email_resend = false;
		} else {
			// Check if the order status is cancelled or refunded.
			$allow_email_resend = ! ( 'cancelled' === $attendee['order_status'] || 'refunded' === $attendee['order_status'] );
		}

		/**
		 *
		 * Shared filter between Woo, EDD, and the default logic.
		 * This filter allows the admin to control the re-send email option when an attendee's email is updated per a payment type (EDD, Woo, etc).
		 * True means allow email resend, false means disallow email resend.
		 *
		 * @since 5.3.1
		 *
		 * @param bool         						  Whether to allow email resending.
		 * @param WP_Post|null $ticket                The ticket post object if available, otherwise null.
		 * @param array|null   $attendee              The attendee information if available, otherwise null.
		 */
		return (bool) apply_filters( 'tribe_tickets_manual_attendee_allow_email_resend', $allow_email_resend, $ticket, $attendee );
	}

	/**
	 * Filters the list of post types that should trigger a cache invalidation on `save_post` to add
	 * all the ones modeling WooCommerce Tickets, Attendees and Orders.
	 *
	 * @since 5.8.0
	 *
	 * @param string[] $post_types The list of post types that should trigger a cache invalidation on `save_post`.
	 *
	 * @return string[] The filtered list of post types that should trigger a cache invalidation on `save_post`.
	 */
	public function filter_cache_listener_save_post_types( array $post_types = [] ): array {
		$post_types[] = $this->ticket_object;
		$post_types[] = self::ATTENDEE_OBJECT;
		$post_types[] = $this->get_active_order_post_type_name();

		return $post_types;
	}

	/**
	 * Includes the "Your Tickets" template for the checkout page.
	 *
	 * This method initializes the Tribe__Template instance, fetches the attendees by the order ID,
	 * and then calls the appropriate template to render the Your Tickets section.
	 *
	 * @since 5.8.0
	 *
	 * @param \WC_Order $order The WooCommerce order object.
	 */
	public function include_your_tickets( $order ): void {
		// Bail if the order is not a valid WC_Order.
		if ( ! $order instanceof \WC_Order ) {
			return;
		}

		// Bail if the order ID is not valid.
		$order_id = $order->get_id();
		if ( empty( $order_id ) ) {
			return;
		}

		$attendees = $this->get_attendees_by_id( $order->get_id() );

		// Bail if there are no attendees.
		if ( empty( $attendees ) ) {
			return;
		}

		// Sort the Attendees by ID.
		$attendee_ids = array_column( $attendees, 'attendee_id' );
		array_multisort( $attendee_ids, SORT_ASC, $attendees );

		$template_args = [
			'provider'      => $this,
			'provider_id'   => $this->class_name,
			'order'         => $order,
			'order_id'      => $order->get_id(),
			'is_tec_active' => tec_tickets_tec_events_is_active(),
			'attendees'     => $attendees,
		];

		tribe( 'tickets.editor.template' )->template( 'components/attendees-list/attendees', $template_args, true );
	}

	/**
	 * Get the active order post_type name.
	 *
	 * @since 5.9.1
	 *
	 * @return string
	 */
	public function get_active_order_post_type_name() {
		if ( ! class_exists( '\Automattic\WooCommerce\Utilities\OrderUtil', false ) ) {
			return $this->order_object;
		}

		// Without table usage return the old Object Order.
		if ( ! OrderUtil::custom_orders_table_usage_is_enabled() ) {
			return $this->order_object;
		}

		// Check if WooCommerce HPOS is enabled, if yes return the HPOS post_type.
		return $this->order_object_hpos;
	}

	/**
	 * Get the attendee order meta data using proper WooCommerce methods.
	 *
	 * @param array|bool|string $meta The original meta data.
	 * @param int               $order_id The order ID.
	 * @param int               $ticket_id The ticket ID.
	 * @param string            $meta_key The meta key.
	 *
	 * @return array|bool|string The modified or original meta data.
	 */
	public function get_proper_attendee_order_meta_data(
		$meta,
		int $order_id,
		int $ticket_id,
		string $meta_key
	) {
		if ( ! empty( $meta ) ) {
			return $meta;
		}

		$order = wc_get_order( $order_id );
		if ( ! $order ) {
			return $meta;
		}

		return $order->get_meta( $meta_key );
	}

	/**
	 * Check to see if WooCommerce HPOS is enabled.
	 *
	 * @since 5.9.3
	 *
	 * @return bool
	 */
	public function is_hpos_enabled(): bool {
		if ( ! class_exists( '\Automattic\WooCommerce\Utilities\OrderUtil', false ) ) {
			return false;
		}
		return OrderUtil::custom_orders_table_usage_is_enabled();
	}
	/**
	 * Checked if the product is a type of ticket.
	 *
	 * @since 5.10.3
	 *
	 * @param int $product_id The Product ID.
	 */
	public function is_ticket( int $product_id ): bool {
		$product = wc_get_product( $product_id );

		if ( ! $product instanceof WC_Product ) {
			return false;
		}

		// Check if the product has the required post meta.
		$event_meta = get_post_meta( $product_id, $this->event_key, true );

		if ( empty( $event_meta ) ) {
			return false;
		}

		return true;
	}

	/**
	 * Get the sale price details for a ticket.
	 *
	 * @since 6.1.5
	 *
	 * @param int $ticket_id The ticket ID.
	 *
	 * @return array
	 */
	public function get_sale_price_details( int $ticket_id ) {
		$wc_ticket_product = wc_get_product( $ticket_id );

		if ( ! $wc_ticket_product instanceof WC_Product ) {
			return [];
		}

		return [
			'enabled'    => $wc_ticket_product->is_on_sale(),
			'sale_price' => $wc_ticket_product->get_sale_price(),
			'start_date' => $wc_ticket_product->get_date_on_sale_from(),
			'end_date'   => $wc_ticket_product->get_date_on_sale_to(),
		];
	}

	/**
	 * Get the quantity of tickets sold for a given event.
	 *
	 * @since 6.0.5
	 *
	 * @param Tribe__Tickets__Ticket_Object $item The ticket object or post object.
	 *
	 * @return int
	 */
	public function get_sold_ticket_quantity( $item ): int {
		$qty               = (int) $item->qty_sold();
		$dispatch_statuses = (array) tribe_get_option( 'tickets-woo-dispatch-status', $this->settings()->get_default_ticket_dispatch_statuses() );
		$completed_only    = count( $dispatch_statuses ) <= 1 && in_array( 'wc-completed', $dispatch_statuses, true );

		if ( ! $completed_only ) {
			$qty += (int) $item->qty_pending();
		}

		return $qty;
	}

	/**
	 * Filter the sold column of the admin tickets table.
	 *
	 * @since 6.0.5
	 *
	 * @param string                        $sold Quantity sold.
	 * @param Tribe__Tickets__Ticket_Object $item The ticket object or post object.
	 *
	 * @return string Formatted price.
	 */
	public function filter_admin_tickets_table_column_sold( $sold, $item ) {
		if ( get_class( $item->get_provider() ) !== get_class( $this ) ) {
			return $sold;
		}

		return $this->get_sold_ticket_quantity( $item );
	}

	/**
	 * Filter the sold column of the admin tickets table.
	 *
	 * @since 6.0.5
	 *
	 * @param string                        $sales Total sales.
	 * @param Tribe__Tickets__Ticket_Object $item The ticket object or post object.
	 *
	 * @return string Formatted price.
	 */
	public function filter_admin_tickets_table_column_sales( $sales, $item ) {
		if ( get_class( $item->get_provider() ) !== get_class( $this ) ) {
			return $sales;
		}

		$sold  = $this->get_sold_ticket_quantity( $item );
		$sales = tribe_format_currency( number_format( (float) $sold * $item->price, 2 ), $item->ID );

		return $sales;
	}

	/**
	 * Decrements the count of total WooCommerce tickets issued/sold for an event/post.
	 *
	 * @since 6.7.0
	 *
	 * @param int $post_id The post ID or the WooCommerce product.
	 * @param int $units   Number of units to decrement by (defaults to 1).
	 *
	 * @return void
	 */
	public function decrement_issued_tickets_count( $post_id, $units = 1 ): void {
		if ( ! $post_id ) {
			return;
		}

		if ( $units <= 0 ) {
			return;
		}

		$issued = (int) get_post_meta( $post_id, self::TOTAL_SALES, true );
		$result = $issued - (int) $units;
		update_post_meta( $post_id, self::TOTAL_SALES, max( $result, 0 ) );
	}
}
