<?php
/**
 * Event helper controller.
 *
 * @since 1.0
 */

declare(strict_types=1);
namespace FeData\FastEvents\Helpers;

defined( 'ABSPATH' ) || exit;

use FeData\FastEvents\Logging\LoggerFactory;
use FeData\FastEvents\Mail\MailerFactory;
use FeData\FastEvents\Mail\MailerInterface;
use FeData\FastEvents\Storage\PluginSettings;
use FeData\FastEvents\Utilities\JsonUtil;
use FeData\FastEvents\Utilities\PluginUtil;
use FeData\FastEvents\Utilities\TimeUtil;
use FeData\FastEvents\Utilities\UniqIds;
use FeData\FastEvents\Utilities\WebhookUtil;
use stdClass;
use Throwable;
use WP_Error;
use WP_REST_Request;
use WP_REST_Response;

/**
 * Helper class.
 */
class EventsHelper {

	public const FAST_EVENTS_CLIENT             = 0;
	public const FAST_EVENTS_MAX_TICKETS        = 1000000;
	public const FAST_EVENTS_CAPTCHA_GOOGLE     = 'https://www.google.com/recaptcha/api/siteverify';
	public const FAST_EVENTS_CAPTCHA_CLOUDFLARE = 'https://challenges.cloudflare.com/turnstile/v0/siteverify';


	/**
	 * Error string for delayed error (in TRANSACTION).
	 *
	 * @since 3.0.0
	 * @var ?string
	 */
	private ?string $delayed_error;

	/**
	 * Id info used for error logging.
	 *
	 * @since 2.3.0
	 * @var array
	 */
	private array $id_info = array();

	/**
	 * Order amount for the event (used for multi-select and passé-partout orders)
	 *
	 * @since 1.0
	 * @var float
	 */
	private float $event_amount;

	/**
	 * Total VAT
	 *
	 * @since 1.0
	 * @var float
	 */
	private float $event_vat;

	/**
	 * Total amount (Can be multiple orders)
	 *
	 * @since 1.0
	 * @var float
	 */
	private float $total_amount = 0.00;

	/**
	 * Total amount of VAT (Can be multiple orders)
	 *
	 * @since 1.0
	 * @var float
	 */
	private float $total_vat = 0.00;

	/**
	 * Number of tickets in this order
	 *
	 * @since 1.0
	 * @var int
	 */
	private int $num_tickets = 0;

	/**
	 * Array of order ids
	 *
	 * @since 1.0
	 * @var int[]
	 */
	private array $order_ids = array();

	/**
	 * Array of event ids
	 *
	 * @since 1.0
	 * @var int[]
	 */
	private array $event_ids = array();

	/**
	 * Array of tickettype names
	 *
	 * @since 1.0
	 * @var array
	 */
	private array $ticket_types = array();

	/**
	 * Array of input field names
	 *
	 * @since 1.0
	 * @var array
	 */
	private array $input_fields = array();

	/**
	 * The name of the event
	 *
	 * @since 1.0
	 * @var string
	 */
	private string $event_name;

	/**
	 * The date of the event
	 *
	 * @since 1.0
	 * @var string
	 */
	private string $event_date;

	/**
	 * The group name of the event (Can be empty)
	 *
	 * @since 1.0
	 * @var string
	 */
	private string $event_group;

	/**
	 * The order UID
	 *
	 * @since 1.0
	 * @var string
	 */
	private string $order_uid;

	/**
	 * Flag (true|false) if test-payments are needed
	 *
	 * @since 1.0
	 * @var bool
	 */
	private bool $event_test_payments_needed;

	/**
	 * SaaS fee
	 *
	 * @since 1.4
	 * @var ?float
	 */
	private ?float $saas_fee;

	/**
	 * SaaS user ID
	 *
	 * @since 1.0
	 * @var int
	 */
	private int $saas_user_id;

	/**
	 * Emailaddress of the user.
	 *
	 * @since 1.0
	 * @var string
	 */
	private string $email;

	/**
	 * The input fields and ticket fields per event
	 *
	 * @since 1.0
	 * @var array
	 */
	private array $json_per_event = array();


	/**
	 * Constructor
	 *
	 * @since 1.0
	 *
	 * @param PluginSettings $settings The plugin settings.
	 * @param LoggerFactory  $logger   The logger.
	 */
	public function __construct(
		private readonly PluginSettings $settings,
		private readonly LoggerFactory $logger,
	) {}


	/**
	 * Process the delayed error (from inside a TRANSACTION).
	 *
	 * @return void
	 * @since 3.0.0
	 */
	public function process_delayed_error(): void {
		if ( ! empty( $this->delayed_error ) ) {
			$context = json_decode( $this->delayed_error, true );
			$error   = $context['error'];
			unset( $context['error'] );
			$this->logger->error( $error, $context );
		}
	}

	/**
	 * Return the grand total ticket costs across the grouped events
	 *
	 * @return float
	 * @since 1.0
	 */
	public function get_amount(): float {
		return $this->total_amount;
	}


	/**
	 * Return ids of the newly created orders
	 *
	 * @return int[]
	 * @since 1.0
	 */
	public function get_order_ids(): array {
		return $this->order_ids;
	}


	/**
	 * Set first order id.
	 *
	 * @param int $order_id The unique order ID.
	 *
	 * @since 1.4
	 */
	public function set_order_ids( int $order_id ): void {
		$this->order_ids[0] = $order_id;
	}


	/**
	 * Return ids of the involved events
	 *
	 * @return int[]
	 * @since 1.0
	 */
	public function get_event_ids(): array {
		return $this->event_ids;
	}


	/**
	 * Return the event name
	 *
	 * @return string
	 * @since 1.0
	 */
	public function get_event_name(): string {
		return $this->event_name;
	}


	/**
	 * Return the emailaddress.
	 *
	 * @return string
	 * @since 1.0
	 */
	public function get_email(): string {
		return $this->email;
	}


	/**
	 * Set the event name
	 *
	 * @param string $event_name The name of the event.
	 *
	 * @since 1.4
	 */
	public function set_event_name( string $event_name ) {
		$this->event_name = $event_name;
	}

	/**
	 * Return the event date
	 *
	 * @return string
	 * @since 1.0
	 */
	public function get_event_date(): string {
		return $this->event_date;
	}


	/**
	 * Return the event group
	 *
	 * @return string
	 * @since 1.0
	 */
	public function get_event_group(): string {
		return $this->event_group;
	}


	/**
	 * Return the order uid
	 *
	 * @return string
	 * @since 1.0
	 */
	public function get_order_uid(): string {
		return $this->order_uid;
	}


	/**
	 * Return the boolean test_payment
	 *
	 * @return bool
	 * @since 1.0
	 */
	public function get_test_payment_needed(): bool {
		return $this->event_test_payments_needed;
	}


	/**
	 * Set the boolean test_payment
	 *
	 * @param bool $test_payments Flag signalling if test payments are needed.
	 *
	 * @since 1.4
	 */
	public function set_test_payment_needed( bool $test_payments ): void {
		$this->event_test_payments_needed = $test_payments;
	}


	/**
	 * Get the SaaS user ID
	 *
	 * @return int The user ID.
	 *
	 * @since 1.4
	 */
	public function get_saas_user_id(): int {
		return $this->saas_user_id;
	}


	/**
	 * Set the SaaS fee
	 *
	 * @return null|float The event SaaS fee.
	 *
	 * @since 1.4
	 */
	public function get_saas_fee(): null|float {
		return $this->saas_fee;
	}


	/**
	 * Return the JSON input_fields (input fields and ticket fields) per order_id
	 *
	 * @param int $order_id Order identifier.
	 *
	 * @return stdClass
	 * @since 1.0
	 */
	public function get_json_input_fields( int $order_id ): stdClass {
		return $this->json_per_event[ $order_id ];
	}


	/**
	 * Return the # of tickets
	 *
	 * @return int
	 * @since 1.0
	 */
	public function get_num_tickets(): int {
		return $this->num_tickets;
	}


	/**
	 * Set the # of tickets
	 *
	 * @param int $num_tickets The number of tickets.
	 *
	 * @since 1.4
	 */
	public function set_num_tickets( int $num_tickets ): void {
		$this->num_tickets = $num_tickets;
	}


	/**
	 * Get price or volume price
	 *
	 * @since 2.2.0
	 * @param stdClass $ticket      Ticket information.
	 * @param int      $num_tickets Number of tickets.
	 * @return string
	 */
	private function get_ticket_price( stdClass $ticket, int $num_tickets ): string {
		if ( empty( $ticket->volume_price ) ) {
			return $ticket->price;
		}
		usort( $ticket->volume_price, fn( $a, $b ) => (int) array_key_first( (array) $b ) - (int) array_key_first( (array) $a ) );
		foreach ( $ticket->volume_price as $vp ) {
			$obj = (array) $vp;
			$key = array_key_first( $obj );
			if ( $num_tickets >= (int) $key ) {
				return (string) $obj[ $key ];
			}
		}
		return $ticket->price;
	}


	/**
	 * Save the order info as JSON
	 *
	 * @param array    $params     Input fields.
	 * @param stdClass $event      Event information.
	 *
	 * @return stdClass|WP_Error
	 *
	 * @since 1.0
	 * @since 2.1.0 Check for min/max values
	 * @since 2.2.0 Removed event_info parameter
	 * @since 2.3.0 Initialize fields and tickets
	 */
	public function create_input_fields( array $params, stdClass $event ): stdClass|WP_Error {
		$total  = 0.00;
		$vtotal = 0.00;
		$cnt    = 0;  // Count tickets ordered.

		$json          = new stdClass();
		$json->fields  = array();
		$json->tickets = array();
		$input_fields  = JsonUtil::get_input_fields( $event->input_fields );

		// Save the extra field info.
		if ( ! empty( $input_fields->fields ) ) {
			foreach ( $input_fields->fields as $item ) {
				if ( ! empty( $item->personalise ) && 1 === $item->personalise ) {
						continue; // This input field needs personalisation -> then we can't use it at order level.
				}

				// Check if an extra input field is present.
				$tid = 'e' . $event->id . $item->id;
				if ( ! isset( $params[ $tid ] ) ) {
					return new WP_Error( 'missing_param', sanitize_text_field( apply_filters( 'fast_events_new_order_text', esc_html__( 'Missing extra input field for this event', 'fast-events' ), 27 ) ), array( 'status' => 400 ) );
				}

				// Check if the field is required and has a value.
				if ( $item->required && empty( $params[ $tid ] ) ) {
					return new WP_Error( 'missing_value', sanitize_text_field( apply_filters( 'fast_events_new_order_text', esc_html__( 'Required extra input field not available', 'fast-events' ), 28 ) ), array( 'status' => 400 ) );
				}

				// Check the 'select' type.
				if ( 'select' === $item->type ) {
					$select_values = explode( ',', $item->value );
					if ( ! in_array( sanitize_text_field( $params[ $tid ] ), $select_values, true ) ) {
							return new WP_Error( 'illegal_value', sanitize_text_field( apply_filters( 'fast_events_new_order_text', esc_html__( 'Illegal value extra input field', 'fast-events' ), 31 ) ), array( 'status' => 400 ) );
					}
				}

				// Check the length fields for text and password.
				if ( 'text' === $item->type || 'password' === $item->type ) {
					if ( ! empty( $item->min ) && strlen( $params[ $tid ] ) < intval( $item->min ) ) {
						/* translators: %1$s is the input field name and %2$s is the minimum length */
						return new WP_Error( 'minimum_length_error', sprintf( __( 'The length of "%1$s" is below %2$s', 'fast-events' ), $item->name, $item->min ), array( 'staus' => 400 ) );
					}
					if ( ! empty( $item->max ) && strlen( $params[ $tid ] ) > intval( $item->max ) ) {
						/* translators: %1$s is the input field name and %2$s is the maximum length */
						return new WP_Error( 'maximum_length_error', sprintf( __( 'The length of "%1$s" is greater then %2$s', 'fast-events' ), $item->name, $item->max ), array( 'staus' => 400 ) );
					}
				} elseif ( 'number' === $item->type ) {
					if ( ! empty( $params[ $tid ] ) && ! is_numeric( $params[ $tid ] ) ) {
						/* translators: %1$s is the input field name */
						return new WP_Error( 'format_error', sprintf( __( 'Non-numeric value for "%1$s"', 'fast-events' ), $item->name ), array( 'staus' => 400 ) );
					}
					if ( ! empty( $params[ $tid ] ) ) {
						if ( ! empty( $item->min ) && intval( $params[ $tid ] ) < intval( $item->min ) ) {
							/* translators: %1$s is the input name and %2$s is the minimum value */
							return new WP_Error( 'minimum_value_error', sprintf( __( 'The value of "%1$s" is below %2$s', 'fast-events' ), $item->name, $item->min ), array( 'staus' => 400 ) );
						}
						if ( ! empty( $item->max ) && intval( $params[ $tid ] ) > intval( $item->max ) ) {
							/* translators: %1$s is the input field name and %2$s is the maximum length */
							return new WP_Error( 'maximum_value_error', sprintf( __( 'The value of "%1$s" is greater then %2$s', 'fast-events' ), $item->name, $item->max ), array( 'staus' => 400 ) );
						}
					}
				} elseif ( 'date' === $item->type ) {
					if ( ! empty( $params[ $tid ] ) ) {
						$date = date_create_from_format( 'Y-m-d', $params[ $tid ] );
						if ( false === $date ) {
							/* translators: %1$s is the input field name */
							return new WP_Error( 'invalid_date', sprintf( __( 'Invalid date field "%1$s", format must be "YYYY-MM-DD"', 'fast-events' ), $item->name ), array( 'staus' => 400 ) );
						}

						if ( ! empty( $item->min ) ) {
							$date_min = date_create_from_format( 'Y-m-d', $item->min );
							if ( $date < $date_min ) {
								/* translators: %1$s is the input field name and %2$s is the minimum date */
								return new WP_Error( 'minimum_date_error', sprintf( __( 'The date of "%1$s" must be later than %2$s', 'fast-events' ), $item->name, $item->min ), array( 'staus' => 400 ) );
							}
						}
						if ( ! empty( $item->max ) ) {
							$date_max = date_create_from_format( 'Y-m-d', $item->max );
							if ( $date > $date_max ) {
								/* translators: %1$s is the input field name and %2$s is the maximum date */
								return new WP_Error( 'maximum_date_error', sprintf( __( 'The date of "%1$s" must be before %2$s', 'fast-events' ), $item->name, $item->max ), array( 'staus' => 400 ) );
							}
						}
					}
				} elseif ( 'time' === $item->type ) {
					if ( ! empty( $params[ $tid ] ) ) {
						$date = date_create_from_format( 'H:i', $params[ $tid ] );
						if ( false === $date ) {
							/* translators: %1$s is the input field name */
							return new WP_Error( 'invalid_time', sprintf( __( 'Invalid time field "%1$s", format must be "HH:MM"', 'fast-events' ), $item->name ), array( 'staus' => 400 ) );
						}

						if ( ! empty( $item->min ) ) {
							$date_min = date_create_from_format( 'H:i', $item->min );
							if ( $date < $date_min ) {
								/* translators: %1$s is the input field name and %2$s is the minimum time */
								return new WP_Error( 'minimum_time_error', sprintf( __( 'The time of "%1$s" must be later than %2$s', 'fast-events' ), $item->name, $item->min ), array( 'staus' => 400 ) );
							}
						}
						if ( ! empty( $item->max ) ) {
							$date_max = date_create_from_format( 'H:i', $item->max );
							if ( $date > $date_max ) {
								/* translators: %1$s is the input field name and %2$s is the maximum time */
								return new WP_Error( 'maximum_time_error', sprintf( __( 'The time of "%1$s" must be before %2$s', 'fast-events' ), $item->name, $item->max ), array( 'staus' => 400 ) );
							}
						}
					}
				}

				// Save input fields.
				$this->input_fields[] = $item->name;
				$json->fields[]       = array(
					'name'  => $item->name,
					'value' => sanitize_text_field( $params[ $tid ] ),
				);
			}
		}

		// Save the ticket info.
		$fields = json_decode( $event->input_fields );
		if ( ! empty( $fields->tickets ) ) {
			foreach ( $fields->tickets as $ticket ) {

				// Check if the ticket input field exists and if min and max are respected.
				$tid = 'e' . $event->id . $ticket->id;
				if ( ! isset( $params[ $tid ] ) ) {
					return new WP_Error( 'missing_param', sanitize_text_field( apply_filters( 'fast_events_new_order_text', esc_html__( 'Missing ticket type: ', 'fast-events' ) . $ticket->name, 26 ) ), array( 'status' => 400 ) );
				}
				if ( (int) $params[ $tid ] < (int) $ticket->min || (int) $params[ $tid ] > (int) $ticket->max ) {
					return new WP_Error( 'wrong_param', sanitize_text_field( apply_filters( 'fast_events_new_order_text', esc_html__( 'Wrong amount for this ticket type: ', 'fast-events' ) . $ticket->name, 32 ) ), array( 'status' => 400 ) );
				}
				$this->ticket_types[] = $ticket->name; // Save ticket types.
				$price                = $this->get_ticket_price( $ticket, absint( $params[ $tid ] ) );
				$json->tickets[]      = array(
					'name'  => $ticket->name,
					'price' => $price,
					'vat'   => $ticket->vat,
					'cflag' => $ticket->cnt,
					'count' => absint( $params[ $tid ] ),
				);
				if ( $ticket->cnt ) {  // Do we need to count this ticket?
					$cnt += absint( $params[ $tid ] );
				}
				$tmp     = absint( $params[ $tid ] ) * (float) $price;
				$total  += $tmp;
				$vtotal += ( $tmp - ( $tmp / ( 1 + ( (float) $ticket->vat / 100 ) ) ) );
			}
		}

		// Increase grant totals (We can have multiple orders, e.g. group_type=2).
		$this->total_amount += $total;
		$this->total_vat    += $vtotal;

		// Set the totals for this event.
		$this->event_amount = $total;
		$this->event_vat    = $vtotal;
		$json->order_amount = number_format( $total, 4, '.', '' );
		$json->order_vat    = number_format( $vtotal, 4, '.', '' );
		$this->num_tickets  = ( 0 === count( $json->tickets ) ) ? 1 : $cnt; // If no tickets are ordered, it is a booking; hence we count 1.

		// Check if there are ticket fields but nothing ordered.
		if ( 0 < count( $json->tickets ) && 0 === $this->num_tickets ) {
			return new WP_Error( 'failed', sanitize_text_field( apply_filters( 'fast_events_new_order_text', esc_html__( 'No tickets ordered', 'fast-events' ), 29 ) ), array( 'status' => 412 ) );
		}

		return $json;
	}


	/**
	 * Sanitize the ticket and input fields after a filter has been applied.
	 * Returns false if the input-field or ticket-type is not recognised.
	 *
	 * @param stdClass $json   Input fields and ticket fields.
	 *
	 * @return bool
	 * @since 1.0
	 */
	private function sanitize_input_and_ticket_fields( stdClass $json ): bool {

		// Walk through all tickets.
		if ( ! empty( $json->tickets ) ) {
			foreach ( $json->tickets as $key => $ticket ) {

				// Is it a valid ticket type?
				if ( ! in_array( $ticket['name'], $this->ticket_types, true ) ) {
					return false;
				}

				// Sanitize values.
				$json->tickets[ $key ]['price'] = number_format( floatval( $ticket['price'] ), 2, '.', '' );
				$json->tickets[ $key ]['vat']   = number_format( floatval( $ticket['vat'] ), 2, '.', '' );

				$cflag                          = absint( $ticket['cflag'] );
				$json->tickets[ $key ]['cflag'] = ( 0 === $cflag ) ? 0 : 1;

				$json->tickets[ $key ]['count'] = absint( $ticket['count'] );
			}
		}

		// Walk through all input fields.
		if ( ! empty( $json->fields ) ) {
			foreach ( $json->fields as $key => $field ) {

				// Is it a valid input field?
				if ( ! in_array( $field['name'], $this->input_fields, true ) ) {
					return false;
				}

				// Sanitize value.
				$json->fields[ $key ]['value'] = sanitize_text_field( $field['value'] );
			}
		}

		return true;
	}


	/**
	 * Recalculate the tickets totals and return info as JSON
	 *
	 * @param stdClass $json   Input fields and ticket fields.
	 *
	 * @return void
	 * @since 1.0
	 */
	private function recalculate_ticket_totals( stdClass $json ): void {
		$total  = 0.00;
		$vtotal = 0.00;
		$cnt    = 0;  // Count tickets ordered.

		// Walk through all tickets.
		if ( ! empty( $json->tickets ) ) {
			foreach ( $json->tickets as $ticket ) {
				if ( (int) $ticket['cflag'] ) {  // Do we need to count this ticket?
					$cnt += (int) $ticket['count'];
				}
				$tmp     = (int) $ticket['count'] * (float) $ticket['price'];
				$total  += $tmp;
				$vtotal += ( $tmp - ( $tmp / ( 1 + ( (float) $ticket['vat'] / 100 ) ) ) );
			}
		}

		// Recalculate new grand totals.
		$this->total_amount -= $this->event_amount;
		$this->total_amount += $total;
		$this->total_vat    -= $this->event_vat;
		$this->total_vat    += $vtotal;

		// Set the new event totals.
		$this->event_amount = $total;
		$this->event_vat    = $vtotal;
		$json->order_amount = number_format( $total, 2, '.', '' );
		$json->order_vat    = number_format( $vtotal, 2, '.', '' );
		$this->num_tickets  = ( 0 === count( $json->tickets ) ) ? 1 : $cnt; // If no tickets are ordered, it is a booking; hence we count 1.
	}


	/**
	 * Clean password fields
	 *
	 * @param stdClass $order_json JSON structure of order input fields, ticket fields, and totals.
	 * @param stdClass $event      Event information.
	 *
	 * @return stdClass
	 * @since 1.0
	 */
	private function clean_password_fields( stdClass &$order_json, stdClass $event ): stdClass {

		// Get the input definition from the event.
		$json  = json_decode( $event->input_fields );
		$input = $json->fields;

		if ( ! empty( $input ) ) {   // No input-fields defined for this event.
			if ( isset( $order_json->fields ) ) {
				foreach ( $input as $item ) {
					if ( 'password' === $item->type ) {
						foreach ( $order_json->fields as $key => $field ) {
							if ( $item->name === $field['name'] ) {
								$order_json->fields[ $key ]['value'] = '';
							}
						}
					}
				}
			}
		}

		return $order_json;
	}


	/**
	 * Check for closed user groups
	 *
	 * Return the #number of tickets a user can purchase. WP_Error will be returned in case of failures.
	 *
	 * @param stdClass $event  Event information.
	 * @param stdClass $json   JSON structure with input-fields, ticket information, and totals.
	 * @param string   $name   Name.
	 * @param string   $email  Emailaddress.
	 *
	 * @return WP_Error|int
	 *
	 * @since 1.0
	 * @since 2.3.0 - Use fast_events_json_encode.
	 *              - Improved error logging.
	 */
	private function check_closed_user_group( stdClass $event, stdClass $json, string $name, string $email ): WP_Error|int {
		global $wpdb;

		// Do we need to check for closed user groups?
		if ( ! $event->user_groups_needed ) {
			return self::FAST_EVENTS_MAX_TICKETS;
		}

		// Is it a private user group?
		$choice = json_decode( $event->input_fields );
		switch ( (int) $choice->user_group->id ) {

			case 0:  // Setting = 'None'.
				return self::FAST_EVENTS_MAX_TICKETS;

			case 1:
				// Check if the WordPress user exists.
				$user = get_user_by( 'email', $email );
				if ( false === $user ) {
					return new WP_Error( 'unknown_user', sanitize_text_field( apply_filters( 'fast_events_new_order_text', esc_html__( 'Unknown user', 'fast-events' ), 23 ) ), array( 'status' => 404 ) );
				}

				// Is there a password field?
				list( $pwd_field, $password ) = $this->check_for_password_field( $event, $json );
				if ( $pwd_field ) {
					if ( false === wp_check_password( $password, $user->data->user_pass, $user->ID ) ) {
						return new WP_Error( 'unknown_user', sanitize_text_field( apply_filters( 'fast_events_new_order_text', esc_html__( 'Invalid password', 'fast-events' ), 24 ) ), array( 'status' => 400 ) );
					}
				}

				// If the roles in the event are empty -> user is good to go.
				if ( '' === $choice->user_group->roles ) {
					return self::FAST_EVENTS_MAX_TICKETS;
				}

				// Check the defined roles against the user roles.
				$arr = explode( ',', $choice->user_group->roles );
				foreach ( $arr as $value ) {
					if ( in_array( $value, $user->roles, true ) ) {
						// The user has the right role.
						return self::FAST_EVENTS_MAX_TICKETS;
					}
				}

				// If we end up here, the user doesn't have the right role.
				return new WP_Error( 'unknown_user', sanitize_text_field( apply_filters( 'fast_events_new_order_text', esc_html__( 'User role mismatch', 'fast-events' ), 25 ) ), array( 'status' => 400 ) );

			case 2:
				// Do we need to use the closed user group from this event or from another event?
				$use_eventid = (int) $choice->user_group->eventid;
				if ( '' === $choice->user_group->eventid || 0 === (int) $choice->user_group->eventid ) {
					$use_eventid = $event->id;
				}

				// Build the query and check if the user is found.
				$count = $wpdb->get_var(
					$wpdb->prepare(
						"SELECT num_tickets FROM {$wpdb->prefix}fe_event_user_groups WHERE email=%s and event_id=%d",
						$email,
						$use_eventid
					)
				);
				if ( (int) $count > 0 ) {
					return (int) $count;
				}
				break;

			case 3:
				// If the 'HTTP header' string doesn't contain the separator ':' -> exit.
				if ( ! str_contains( $choice->user_group->rest_header, ':' ) ) {
					return 0;
				}

				// Build the body.
				$body               = new stdClass();
				$body->name         = $name;
				$body->email        = $email;
				$body->fields       = $json->fields;
				$body->tickets      = $json->tickets;
				$body->order_amount = $json->order_amount;
				$body->order_vat    = $json->order_vat;

				// Build the REST API query.
				$arr      = explode( ':', $choice->user_group->rest_header );
				$response = wp_remote_post(
					$choice->user_group->rest_url,
					array(
						'method'  => 'POST',
						'timeout' => 5,
						'headers' =>
							array(
								'Content-Type'  => 'application/json',
								trim( $arr[0] ) => trim( $arr[1] ),
							),
						'body'    => JsonUtil::fast_events_json_encode( $body ),
					)
				);

				// Network error?
				if ( is_wp_error( $response ) ) {
					$this->delayed_error = JsonUtil::fast_events_json_encode(
						array(
							'event_id' => (int) $event->id,
							'order_id' => 0,
							'type'     => 'REST API',
							'url'      => $choice->user_group->rest_url,
							'error'    => 'Rest API post: ' . $response->get_error_message(),
							...$this->id_info,
						)
					);
					return new WP_Error( 'server_error', $response->get_error_message(), array( 'status' => 400 ) );
				}

				$result = json_decode( wp_remote_retrieve_body( $response ) );
				if ( null === $result || ! isset( $result->count ) ) {
					$this->delayed_error = JsonUtil::fast_events_json_encode(
						array(
							'event_id' => (int) $event->id,
							'order_id' => 0,
							'type'     => 'REST API',
							'url'      => $choice->user_group->rest_url,
							'error'    => 'Rest API post: Invalid output from server',
							...$this->id_info,
						)
					);
					return new WP_Error( 'server_error', esc_html__( 'Invalid output from REST API server', 'fast-events' ), array( 'status' => 400 ) );
				}
				$http_code = (int) wp_remote_retrieve_response_code( $response );
				if ( 200 > $http_code || 299 < $http_code ) {
					$this->delayed_error = JsonUtil::fast_events_json_encode(
						array(
							'event_id'  => (int) $event->id,
							'order_id'  => 0,
							'log_type'  => 'REST API',
							'http_code' => ! isset( $result->error ) ? (int) wp_remote_retrieve_response_code( $response ) : 400,
							'error'     => 'Invalid output from REST API server' . ! isset( $result->error ) ? '' : $result->error,
							...$this->id_info,
						),
					);
					if ( ! isset( $result->error ) ) {
						/* translators: %d is the HTTP code */
						return new WP_Error( 'server_error', sprintf( esc_html__( 'Server returned HTTP-code: %d', 'fast-events' ), (int) wp_remote_retrieve_response_code( $response ) ), array( 'status' => 412 ) );
					} else {
						return new WP_Error( 'server_error', $result->error, array( 'status' => 400 ) );
					}
				}

				return absint( $result->count );
		}

		// Nothing found.
		return 0;
	}




	/**
	 * Check if there is a password field in the order input
	 *
	 * @param stdClass $event       Event information.
	 * @param stdClass $order_json  JSON structure with order, input-fields, tickets-fields, and totals.
	 *
	 * @return array
	 */
	private function check_for_password_field( stdClass $event, stdClass $order_json ): array {
		$json = JsonUtil::get_input_fields( $event->input_fields );

		if ( ! empty( $json->fields ) ) {   // No input-fields defined for this event.
			if ( ! empty( $order_json->fields ) ) {
				foreach ( $json->fields as $item ) {
					if ( 'password' === $item->type ) {
						foreach ( $order_json->fields as $key => $field ) {
							if ( $item->name === $field['name'] ) {
								return array( true, $field['value'] );
							}
						}
					}
				}
			}
		}

		return array( false, '' );
	}


	/**
	 * Check for unique users (User can only once book/order)
	 *
	 * @param stdClass $event  Event information.
	 * @param string   $email  Emailaddress.
	 *
	 * @return bool
	 */
	private function check_unique_users( stdClass $event, string $email ): bool {
		global $wpdb;

		$arr = array();

		// Do we need to check for unique users?
		if ( ! $event->unique_users_needed ) {
			return false;
		}

		// If there is no id or id=0, we assume the current event.
		$choice = json_decode( $event->input_fields );
		if ( '' === $choice->unique_users->master_eventids || '0' === $choice->unique_users->master_eventids ) {
			$arr[0] = $event->id;
		} else {
			$arr = explode( ',', $choice->unique_users->master_eventids );
		}

		// Build the query.
		$table_name = $wpdb->prefix . 'fe_event_orders';
		$query      = "SELECT COUNT(*) FROM $table_name WHERE email=%s AND payment_status in ('paid','open','booked','booked - confirmed','dashboard','dashboard - confirmed') AND event_id in (";
		foreach ( $arr as $item ) {
			$query .= filter_var( $item, FILTER_SANITIZE_NUMBER_INT ) . ',';
		}
		$query = rtrim( $query, ',' ) . ')';
		$count = $wpdb->get_var( $wpdb->prepare( $query, $email ) ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
		if ( $count > 0 ) {
			return true;
		}

		// Found no booking/order.
		return false;
	}


	/**
	 * Update the stock of the event
	 *
	 * @param array    $params Input from form.
	 * @param stdClass $event  Event information.
	 * @return WP_Error|bool
	 *
	 * @since 1.0
	 * @since 2.3.0 Use fast_events_json_encode.
	 */
	private function update_available_stock( array $params, stdClass $event ): WP_Error|bool {
		global $wpdb;

		// Check the ticket-type stock first.
		$c_tickets = array(); // Array of ticket names with the number sold (Used for linked event).
		$fields    = json_decode( $event->input_fields );
		if ( ! empty( $fields->tickets ) ) {
			foreach ( $fields->tickets as $key => $value ) {

				// Only if there is stock control on ticket-type and something is ordered.
				$tid                       = 'e' . $event->id . $value->id;
				$c_tickets[ $value->name ] = absint( $params[ $tid ] ); // Save it for a linked event.
				if ( '' !== $value->stock && 0 !== absint( $params[ $tid ] ) ) {
					if ( ( absint( $params[ $tid ] ) + $value->sold ) > $value->stock ) {
						return new WP_Error(
							'failed',
							sprintf(
								/* translators: %1$s is the name of the ticket-type and %2$s is the event name */
								sanitize_text_field( apply_filters( 'fast_events_new_order_text', esc_html__( 'Not enough stock available for ticket-type: %1$s (%2$s)', 'fast-events' ), 21 ) ),
								$value->name,
								$event->event_name
							),
							array( 'status' => 412 )
						);
					}
				}

				// Increase sold tickets for this ticket-type.
				$fields->tickets[ $key ]->sold += absint( $params[ $tid ] );
			}
		}

		// First part of the query.
		$new_fields = JsonUtil::fast_events_json_encode( $fields );
		$query      = "UPDATE {$wpdb->prefix}fe_events SET input_fields=%s, sold=sold+%d WHERE id=%d";

		// Build the update query, if event stock=0, there is no stock control.
		if ( 0 === (int) $event->stock ) {
			$result = $wpdb->query( $wpdb->prepare( $query, $new_fields, $this->num_tickets, $event->id ) ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
		} else {
			$query .= ' AND sold+%d<=stock';
			$result = $wpdb->query( $wpdb->prepare( $query, $new_fields, $this->num_tickets, $event->id, $this->num_tickets ) ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
		}

		// Analyse the update.
		if ( false === $result || 0 === $result ) {
			return false;  // No stock available or an error.
		}

		// Check if the stock control is linked to another event.
		if ( 0 === (int) $event->stock_link ) {
			return true;
		}

		// We are dealing with a linked event. Increase matched ticket types and stock of the linked event.
		$tmp_event = $wpdb->get_row(
			$wpdb->prepare(
				"SELECT id, stock, sold, input_fields FROM {$wpdb->prefix}fe_events WHERE id = %d FOR UPDATE",
				absint( $event->stock_link )
			)
		);
		if ( empty( $tmp_event ) ) {  // Should not happen unless the user screwed up the input.
			return false;
		}

		// Check the stock event level only if it is not 0.
		if ( 0 !== (int) $tmp_event->stock ) {
			if ( ( $this->num_tickets + (int) $tmp_event->sold ) > (int) $tmp_event->stock ) {
				return false;
			}
		}
		$tmp_event->sold += $this->num_tickets;

		// Match ticket types and increase stock.
		$l_fields = json_decode( $tmp_event->input_fields );
		if ( ! empty( $l_fields->tickets ) ) {
			foreach ( $l_fields->tickets as $key => $value ) {

				// Does the ticket name match anyone sold?
				if ( isset( $c_tickets[ $value->name ] ) ) {

					// Is there stock control?
					if ( '' !== $value->stock ) {

						// Is there enough stock?
						if ( ( $c_tickets[ $value->name ] + $value->sold ) > $value->stock ) {
							return new WP_Error(
								'failed',
								sprintf(
									/* translators: %1$s is the name of the ticket-type and %2$s is the event name */
									sanitize_text_field( apply_filters( 'fast_events_new_order_text', esc_html__( 'Not enough stock available for ticket-type: %1$s (%2$s)', 'fast-events' ), 21 ) ),
									$value->name,
									$event->event_name
								),
								array( 'status' => 412 )
							);
						}
					}

					// Increase ticket type stock.
					$l_fields->tickets[ $key ]->sold += $c_tickets[ $value->name ];
				}
			}
		}
		$linked_result = $wpdb->update(
			$wpdb->prefix . 'fe_events',
			array(
				'sold'         => $tmp_event->sold,
				'input_fields' => JsonUtil::fast_events_json_encode( $l_fields ),
			),
			array(
				'id' => $tmp_event->id,
			)
		);

		// Analyse the update.
		if ( false === $linked_result || 0 === $linked_result ) {
			return false;  // No stock available or an error.
		}

		// Stock updated successfully.
		return true;
	}


	/**
	 * Restock tickets after payment failures, refunds, deletes, admin order deletes and payment expiration
	 *
	 * @param array $orders Array of order ids.
	 * @return void
	 *
	 * @since 1.0
	 * @since 2.3.0 Use fast_events_json_encode.
	 */
	public function re_stock_tickets( array $orders ): void {
		global $wpdb;

		// Look up the event.
		$order_table = $wpdb->prefix . 'fe_event_orders';
		$event_table = $wpdb->prefix . 'fe_events';
		foreach ( $orders as $order ) {
			$events = array();
			// phpcs:disable
			$result = $wpdb->get_row(
				$wpdb->prepare(
					"SELECT $order_table.id, $order_table.num_tickets, $order_table.event_id, " .
					"$order_table.input_fields, $event_table.stock_link, $event_table.group_type " .
					"FROM $order_table " .
					"INNER JOIN $event_table ON $event_table.id = $order_table.event_id " .
					"WHERE $order_table.id = %d",
					$order
				)
			);
			// phpcs:enable
			// Does the key exist?
			if ( empty( $result ) ) {
				$this->logger->error(
					'Restock order #' . $order . ' error: ' . $wpdb->last_error,
					array(
						'event_id' => 0,
						'order_id' => (int) $order,
						'type'     => 'RESTOCK',
					)
				);
				return;
			}

			if ( 3 === (int) $result->group_type ) {
				$json = json_decode( $result->input_fields );
				foreach ( $json->events as $event ) {
					$events[] = $event;
				}
			}
			$events[] = $result->event_id;

			// Get an array with ticket-types sold.
			$fields     = json_decode( $result->input_fields );
			$types_sold = array();
			foreach ( $fields->tickets as $key => $value ) {
				$types_sold[ $value->name ] = $value->count;
			}

			// Start the transaction.
			$wpdb->query( 'START TRANSACTION' );

			// Walk through all events.
			foreach ( $events as $item ) {

				$row = $wpdb->get_row( $wpdb->prepare( "SELECT id, input_fields, stock_link FROM $event_table WHERE id = %d FOR UPDATE", $item ) ); // phpcs:ignore
				if ( empty( $row ) ) {
					$tmp_error = $wpdb->last_error;
					$wpdb->query( 'ROLLBACK' );
					$this->logger->error(
						'Restock order #' . $order . ' error: ' . $tmp_error,
						array(
							'event_id' => (int) $item,
							'order_id' => (int) $order,
							'type'     => 'RESTOCK',
						)
					);
					return;
				}

				// Update sold ticket types and event sold tickets.
				$ev_fields = json_decode( $row->input_fields );
				foreach ( $ev_fields->tickets as $key => $value ) {
					if ( isset( $types_sold[ $value->name ] ) ) {
						$ev_fields->tickets[ $key ]->sold -= $types_sold[ $value->name ];
						if ( 0 > $ev_fields->tickets[ $key ]->sold ) {
							$ev_fields->tickets[ $key ]->sold = 0;
						}
					}
				}
				$resp = $wpdb->query(
					$wpdb->prepare(
						"UPDATE $event_table SET input_fields=%s, sold = GREATEST(sold - %d, 0) WHERE id=%d", // phpcs:ignore
						JsonUtil::fast_events_json_encode( $ev_fields ),
						(int) $result->num_tickets,
						(int) $row->id
					)
				);
				if ( false === $resp || 0 === $resp ) {
					$tmp_error = $wpdb->last_error;
					$wpdb->query( 'ROLLBACK' );
					$this->logger->error(
						'Restock order #' . $order . ' error: ' . $tmp_error,
						array(
							'event_id' => (int) $item,
							'order_id' => (int) $order,
							'type'     => 'RESTOCK',
						)
					);
					return;
				}

				// Check if the stock control is linked to another event.
				if ( 0 !== (int) $row->stock_link ) {
					$linked_event = $wpdb->get_row(
						$wpdb->prepare(
							"SELECT id, sold, input_fields FROM $event_table WHERE id = %d FOR UPDATE", // phpcs:ignore
							(int) $row->stock_link
						)
					);
					if ( empty( $linked_event ) ) {
						$wpdb->query( 'ROLLBACK' );
						$this->logger->error(
							'Restock: Can not find linked event #' . $row->stock_link,
							array(
								'event_id'     => (int) $item,
								'order_id'     => (int) $order,
								'type'         => 'RESTOCK',
								'linked_event' => (int) $row->stock_link,
							)
						);
						return;
					}

					// Match ticket types and decrease stock.
					$l_fields = json_decode( $linked_event->input_fields );
					if ( ! empty( $l_fields->tickets ) ) {
						foreach ( $l_fields->tickets as $key => $value ) {

							// Does the ticket name match anyone sold? If so, decrease it.
							if ( isset( $types_sold[ $value->name ] ) ) {
								$l_fields->tickets[ $key ]->sold -= $types_sold[ $value->name ];
								if ( 0 > $l_fields->tickets[ $key ]->sold ) {
									$l_fields->tickets[ $key ]->sold = 0;
								}
							}
						}
					}

					$resp = $wpdb->query(
						$wpdb->prepare(
							"UPDATE $event_table SET input_fields=%s, sold = GREATEST(sold - %d, 0) WHERE id=%d", // phpcs:ignore
							JsonUtil::fast_events_json_encode( $l_fields ),
							(int) $result->num_tickets,
							(int) $linked_event->id
						)
					);

					// Analyse the update.
					if ( false === $resp || 0 === $resp ) {
						$tmp_error = $wpdb->last_error;
						$wpdb->query( 'ROLLBACK' );
						$this->logger->error(
							'Restock tickets for order #' . $order . ' error: ' . $tmp_error,
							array(
								'event_id'     => (int) $item,
								'order_id'     => (int) $order,
								'type'         => 'RESTOCK',
								'linked_event' => (int) $row->stock_link,
							)
						);
						return;
					}
				}
			}

			// Commit the transaction.
			$wpdb->query( 'COMMIT' );
		}
	}


	/**
	 * Process the passé-partout event
	 *
	 * @param string   $event_group Event info.
	 * @param stdClass $json        Input fields.
	 * @return array|WP_Error
	 *
	 * @since 1.0
	 * @since 2.3.0 Use fast_events_json_encode.
	 */
	private function update_passe_partout_child_stock( string $event_group, stdClass $json ): WP_Error|array {
		global $wpdb;

		$arr = array();

		// Create an array of ticket-names with tickets sold.
		$types_sold = array();
		foreach ( $json->tickets as $key => $value ) {
			$types_sold[ $value['name'] ] = $value['count'];
		}

		// Get the associated events of the passe-partout.
		$result = $wpdb->get_results(
			$wpdb->prepare(
				"SELECT id FROM {$wpdb->prefix}fe_events WHERE event_group = %s AND group_type = 2 ORDER BY id",
				$event_group
			)
		);
		if ( null === $result ) {
			return new WP_Error( 'no_event', sanitize_text_field( apply_filters( 'fast_events_new_order_text', esc_html__( 'No child events found for passe-partout', 'fast-events' ), 22 ) ), array( 'status' => 404 ) );
		}

		// Create array and process stock of associated events.
		foreach ( $result as $row ) {
			$arr[] = (int) $row->id;

			// Get the event.
			$event = $wpdb->get_row(
				$wpdb->prepare(
					"SELECT id, event_name, stock, stock_link, sold, input_fields FROM {$wpdb->prefix}fe_events WHERE id = %d FOR UPDATE",
					$row->id
				)
			);
			if ( empty( $event ) ) {
				/* translators: %d is the event_id */
				return new WP_Error( 'no_event', sprintf( sanitize_text_field( apply_filters( 'fast_events_new_order_text', esc_html__( 'Event %d does not exist', 'fast-events' ), 14 ) ), $row->id ), array( 'status' => 404 ) );
			}

			// Update sold ticket types and event sold tickets.
			$ev_fields = json_decode( $event->input_fields );
			foreach ( $ev_fields->tickets as $key => $value ) {
				if ( isset( $types_sold[ $value->name ] ) ) {
					if ( '' !== $value->stock && 0 !== (int) $types_sold[ $value->name ] ) {
						if ( ( (int) $types_sold[ $value->name ] + $value->sold ) > $value->stock ) {
							return new WP_Error(
								'failed',
								sprintf(
									/* translators: %1$s is the name of the ticket-type and %2$s is the event name */
									sanitize_text_field( apply_filters( 'fast_events_new_order_text', esc_html__( 'Not enough stock available for ticket-type: %1$s (%2$s)', 'fast-events' ), 21 ) ),
									$value->name,
									$event->event_name
								),
								array( 'status' => 412 )
							);
						}
					}
					$ev_fields->tickets[ $key ]->sold += (int) $types_sold[ $value->name ];
				}
			}

			// First part of the query.
			$new_fields = JsonUtil::fast_events_json_encode( $ev_fields );
			$query      = "UPDATE {$wpdb->prefix}fe_events SET input_fields=%s, sold=sold+%d WHERE id=%d";

			// Build the update query, if event stock=0, there is no stock control.
			if ( 0 === (int) $event->stock ) {
				$result = $wpdb->query( $wpdb->prepare( $query, $new_fields, $this->num_tickets, $event->id ) ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
			} else {
				$query .= ' AND sold+%d<=stock';
				$result = $wpdb->query( $wpdb->prepare( $query, $new_fields, $this->num_tickets, $event->id, $this->num_tickets ) ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
			}

			// Analyse the update.
			if ( false === $result || 0 === $result ) {
				/* translators: %s is the name of the event */
				return new WP_Error( 'failed', sprintf( sanitize_text_field( apply_filters( 'fast_events_new_order_text', esc_html__( 'Stock update failed for "%s"', 'fast-events' ), 20 ) ), $event->event_name ), array( 'status' => 412 ) );
			}

			// Check if the stock control is linked to another event.
			if ( 0 !== (int) $event->stock_link ) {
				$link_event = $wpdb->get_row(
					$wpdb->prepare(
						"SELECT id, event_name, stock, sold, input_fields FROM {$wpdb->prefix}fe_events WHERE id = %d FOR UPDATE",
						absint( $event->stock_link )
					)
				);
				if ( empty( $link_event ) ) {
					/* translators: %d is the id of the event */
					return new WP_Error( 'failed', sprintf( esc_html__( 'Event "%d" not found', 'fast-events' ), absint( $event->stock_link ) ), array( 'status' => 412 ) );
				}

				// Check the stock event level only if it is not 0.
				if ( 0 !== (int) $link_event->stock ) {
					if ( ( $this->num_tickets + (int) $link_event->sold ) > (int) $link_event->stock ) {
						/* translators: %d is the id of the event */
						return new WP_Error( 'failed', sprintf( sanitize_text_field( apply_filters( 'fast_events_new_order_text', esc_html__( 'Not enough stock for linked event-id %d', 'fast-events' ), 30 ) ), $event->stock_link ), array( 'status' => 412 ) );
					}
				}
				$link_event->sold += $this->num_tickets;

				// Match ticket types and increase stock.
				$l_fields = json_decode( $link_event->input_fields );
				if ( ! empty( $l_fields->tickets ) ) {
					foreach ( $l_fields->tickets as $key => $value ) {

						// Does the ticket name match anyone sold?
						if ( isset( $types_sold[ $value->name ] ) ) {

							// Is there stock control for the ticket type?
							if ( '' !== $value->stock ) {

								// Is there enough stock?
								if ( ( $types_sold[ $value->name ] + $value->sold ) > $value->stock ) {
									return new WP_Error(
										'failed',
										sprintf(
											/* translators: %1$s is the name of the ticket-type and %2$s is the event name */
											sanitize_text_field( apply_filters( 'fast_events_new_order_text', esc_html__( 'Not enough stock available for ticket-type: %1$s (%2$s)', 'fast-events' ), 21 ) ),
											$value->name,
											$event->event_name
										),
										array( 'status' => 412 )
									);
								}
							}

							// Increase ticket type stock.
							$l_fields->tickets[ $key ]->sold += (int) $types_sold[ $value->name ];
						}
					}
				}

				// Build the update query; if stock=0, there is no stock control.
				$ppt_result = $wpdb->update(
					$wpdb->prefix . 'fe_events',
					array(
						'sold'         => $link_event->sold,
						'input_fields' => JsonUtil::fast_events_json_encode( $l_fields ),
					),
					array(
						'id' => $link_event->id,
					)
				);

				// Analyse the update.
				if ( false === $ppt_result || 0 === $ppt_result ) {
					/* translators: %s is the name of the event */
					return new WP_Error( 'failed', sprintf( sanitize_text_field( apply_filters( 'fast_events_new_order_text', esc_html__( 'Stock update failed for "%s"', 'fast-events' ), 20 ) ), $event->event_name ), array( 'status' => 412 ) );
				}
			}
		}

		return $arr;
	}


	/**
	 * Validate and calculate coupon amount
	 *
	 * @param stdClass $json     Order values (from 'create_input_fields', see example).
	 * @param string   $code     The coupon code.
	 * @param string   $event_id The unique id of the event.
	 * @param string   $email    The email address of the orderer.
	 * @param bool     $update   Do we need to update the values (true=yes, false=no -> default)?
	 *
	 *  Example $json:
	 * {
	 *  "fields": [ {
	 *      "name": "Entrance",
	 *      "value": "South"
	 *    }, {
	 *      "name": "Favoriet",
	 *      "value": "Pink Floyd"
	 *    }
	 * ],
	 * "tickets": [ {
	 *      "name": "Silver",
	 *      "price": "25",
	 *      "vat": "21",
	 *      "cflag": 1,
	 *      "count": 2
	 *    }, {
	 *      "name": "Gold (Backstage)",
	 *      "price": "40",
	 *      "vat": "21",
	 *      "cflag": 1,
	 *      "count": 0
	 *    }
	 * ],
	 * "order_amount": "50.00",
	 * "order_vat": "8.68",
	 * "events": [9],
	 * "orders": [39]
	 * }.
	 *
	 * @return WP_Error|float
	 *
	 * @since 2.2.0
	 */
	public function check_coupon( stdClass &$json, string $code, string $event_id, string $email, bool $update = false ): WP_Error|float {
		global $wpdb;

		// Get the coupon.
		$query_suffix = $update ? 'FOR UPDATE' : '';
		$coupon_table = $wpdb->prefix . 'fe_event_coupons';
		$coupon       = $wpdb->get_row( $wpdb->prepare( "SELECT * FROM $coupon_table WHERE code = %s $query_suffix", $code ) ); //phpcs:ignore
		if ( empty( $coupon ) ) {
			/* translators: %s is the coupon code */
			return new WP_Error( 'no_coupon', sprintf( sanitize_text_field( apply_filters( 'fast_events_new_order_text', esc_html__( 'The coupon code %s does not exist', 'fast-events' ), 33 ) ), $code ), array( 'status' => 404 ) );
		}

		// Is the coupon already active?
		if ( TimeUtil::local_time() < TimeUtil::date_local_time( $coupon->start_date ) ) {
			return new WP_Error( 'coupon_not_active', sanitize_text_field( apply_filters( 'fast_events_new_order_text', esc_html__( 'The discount code is not yet active', 'fast-events' ), 34 ) ), array( 'status' => 400 ) );
		}

		// Is the coupon expired?
		if ( TimeUtil::local_time() > TimeUtil::date_local_time( $coupon->end_date ) ) {
			return new WP_Error( 'coupon_not_active', sanitize_text_field( apply_filters( 'fast_events_new_order_text', esc_html__( 'The discount code has expired', 'fast-events' ), 35 ) ), array( 'status' => 400 ) );
		}

		// Check if the usage limit is reached.
		if ( (int) $coupon->used >= (int) $coupon->usage_limit ) {
			return new WP_Error( 'maximum_reached', sanitize_text_field( apply_filters( 'fast_events_new_order_text', esc_html__( 'The maximum number of times the discount code can be used has been reached', 'fast-events' ), 36 ) ), array( 'status' => 400 ) );
		}

		// Check for a connected email address.
		if ( ! empty( $coupon->email ) && strtolower( $coupon->email ) !== $email ) {
			return new WP_Error( 'wrong_email', sanitize_text_field( apply_filters( 'fast_events_new_order_text', esc_html__( 'This discount code is linked to another email address', 'fast-events' ), 37 ) ), array( 'status' => 400 ) );
		}

		// Check if this coupon is part of specific events.
		if ( ! empty( $coupon->events ) && ! in_array( $event_id, explode( ',', $coupon->events ), true ) ) {
			return new WP_Error( 'wrong_event', sanitize_text_field( apply_filters( 'fast_events_new_order_text', esc_html__( 'This discount code is not linked to this event', 'fast-events' ), 38 ) ), array( 'status' => 400 ) );
		}

		// Calculate the # of tickets and the amount and check if there is a limit on which tickets to use.
		$vat_percentage      = 0;
		$num_tickets         = 0;
		$pre_discount_amount = 0.00;
		$pre_discount_vat    = 0.00;
		$allowed_tickets     = empty( $coupon->tickets ) ? array() : explode( ',', $coupon->tickets );
		foreach ( $json->tickets as $ticket ) {
			if ( ! empty( $coupon->tickets ) && ! in_array( $ticket['name'], $allowed_tickets, true ) ) {
				continue;
			}
			$vat_percentage       = (float) $ticket['vat'];
			$num_tickets         += $ticket['count'];
			$tmp                  = $ticket['count'] * (float) $ticket['price'];
			$pre_discount_amount += $tmp;
			$pre_discount_vat    += ( $tmp - ( $tmp / ( 1 + ( (float) $ticket['vat'] / 100 ) ) ) );
		}
		$display_tickets = empty( $coupon->tickets ) ? '' : '(' . $coupon->tickets . ')';

		// Is there a minimum number of tickets needed?
		if ( ! empty( $coupon->minimum_tickets ) && (int) $coupon->minimum_tickets > $num_tickets ) {
			/* translators: %1$s: the minimum number of tickets, %2$s: comma-separated list of ticket types */
			return new WP_Error( 'minimum_tickets_needed', sprintf( sanitize_text_field( apply_filters( 'fast_events_new_order_text', esc_html__( 'This discount code requires a minimum of %1$s tickets %2$s', 'fast-events' ), 39 ) ), $coupon->minimum_tickets, $display_tickets ), array( 'status' => 400 ) );
		}

		// Is there a maximum number of tickets allowed?
		if ( ! empty( $coupon->maximum_tickets ) && (int) $coupon->maximum_tickets < $num_tickets ) {
			/* translators: %1$s: the maximum number of tickets, %2$s: comma-separated list of ticket types */
			return new WP_Error( 'maximum_tickets_allowed', sprintf( sanitize_text_field( apply_filters( 'fast_events_new_order_text', esc_html__( 'This discount code allows a maximum of %1$s tickets %2$s', 'fast-events' ), 40 ) ), $coupon->maximum_tickets, $display_tickets ), array( 'status' => 400 ) );
		}

		// Is there a minimum amount needed?
		if ( ! empty( $coupon->minimum_amount ) && (float) $coupon->minimum_amount > $pre_discount_amount ) {
			/* translators: %1$s: the minimum amount, %2$s: comma-separated list of ticket types */
			return new WP_Error( 'minimum_amount_needed', sprintf( sanitize_text_field( apply_filters( 'fast_events_new_order_text', esc_html__( 'A minimum amount of %1$s %2$s is required for this discount code', 'fast-events' ), 41 ) ), number_format( (float) $coupon->minimum_amount, 2 ), $display_tickets ), array( 'status' => 400 ) );
		}

		// Is there a maximum amount allowed?
		if ( ! empty( $coupon->maximum_amount ) && (float) $coupon->maximum_amount < $pre_discount_amount ) {
			/* translators: %1$s: the maximum amount, %2$s: comma-separated list of ticket types */
			return new WP_Error( 'maximum_amount_allowed', sprintf( sanitize_text_field( apply_filters( 'fast_events_new_order_text', esc_html__( 'A maximum amount of %1$s %2$s is allowed for this discount code', 'fast-events' ), 42 ) ), number_format( (float) $coupon->maximum_amount, 2 ), $display_tickets ), array( 'status' => 400 ) );
		}

		// Check the usage limit per user.
		$usage       = 0;
		$order_table = $wpdb->prefix . 'fe_event_orders';
		$orders      = $wpdb->get_results( $wpdb->prepare( "SELECT input_fields FROM $order_table WHERE email = %s AND discount != 0", $email ) ); // phpcs:ignore
		if ( ! empty( $orders ) ) {
			foreach ( $orders as $order ) {
				$inp = json_decode( $order->input_fields );
				if ( ! empty( $inp->discounts ) ) {
					foreach ( $inp->discounts as $discount ) {
						if ( strtolower( $discount->code ) === strtolower( $code ) ) {
							++$usage;
						}
					}
				}
			}
			if ( $coupon->usage_limit_per_user <= $usage ) {
				/* translators: %s: The maximum number of times the discount code can be used */
				return new WP_Error( 'maximum_used_per_user', sprintf( sanitize_text_field( apply_filters( 'fast_events_new_order_text', esc_html__( 'The maximum number of times the discount code can be used is: %s', 'fast-events' ), 43 ) ), $coupon->usage_limit_per_user ), array( 'status' => 400 ) );
			}
		}

		// Calculate the discount.
		if ( 'fixed' === $coupon->type ) {
			$discount = (float) $coupon->amount;
		} else {
			$discount = $pre_discount_amount * ( (float) $coupon->amount / 100 );
		}

		// If it's not an update, stop here.
		if ( ! $update ) {
			return $discount;
		}

		// Add it to the JSON structure and update amount and vat.
		$json->discounts    = array();
		$json->discounts[]  = array(
			'code'   => $code,
			'amount' => $discount,
		);
		$new_amount         = $pre_discount_amount - $discount;
		$new_vat            = $new_amount * ( $vat_percentage / ( $vat_percentage + 100 ) );
		$this->total_amount = $this->total_amount - $pre_discount_amount + $new_amount;
		$this->total_vat    = $this->total_vat - $pre_discount_vat + $new_vat;
		$json->order_amount = number_format( (float) $json->order_amount - $pre_discount_amount + $new_amount, 4 );
		$json->order_vat    = number_format( (float) $json->order_vat - $pre_discount_vat + $new_vat, 4 );
		$this->event_amount = $this->event_amount - $pre_discount_amount + $new_amount;
		$this->event_vat    = $this->event_vat - $pre_discount_vat + $new_vat;

		// Update the coupon table.
		$result = $wpdb->query( $wpdb->prepare( "UPDATE $coupon_table SET used = used + 1, last_used = NOW() WHERE id = %d ", $coupon->id ) ); // phpcs:ignore
		if ( false === $result ) {
			return new WP_Error( 'failed', __( 'Update coupon failed', 'fast-events' ) . ': ' . $wpdb->last_error, array( 'status' => 400 ) );
		}

		return $discount;
	}


	/**
	 * Check the event-form name and email
	 *
	 * @param array $params   Input fields from the form.
	 *
	 * @return WP_Error|array
	 *
	 * @since 2.2.0
	 * @since 2.3.0 Added id_info for future error logging.
	 */
	public function validate_name_and_email( array $params ): WP_Error|array {

		// Check if mandatory fields and if 'name' and 'email' are not empty.
		if ( empty( $params['name'] ) || empty( $params['email'] ) ) {
			return new WP_Error( 'missing_param', sanitize_text_field( apply_filters( 'fast_events_new_order_text', esc_html__( 'Mandatory input not available', 'fast-events' ), 2 ) ), array( 'status' => 404 ) );
		}

		$name  = trim( $params['name'] );
		$email = strtolower( $params['email'] );

		// Check for a malformed email address.
		if ( ! filter_var( $email, FILTER_VALIDATE_EMAIL ) ) {
			return new WP_Error( 'failed', sanitize_text_field( apply_filters( 'fast_events_new_order_text', esc_html__( 'Invalid emailaddress', 'fast-events' ), 3 ) ), array( 'status' => 412 ) );
		}

		// Check name length (<=50).
		if ( 50 < strlen( $name ) ) {
			return new WP_Error( 'failed', sanitize_text_field( apply_filters( 'fast_events_new_order_text', esc_html__( 'Name is too long (Max 50 chars)', 'fast-events' ), 4 ) ), array( 'status' => 412 ) );
		}

		// Check email length (<=100).
		if ( 100 < strlen( $email ) ) {
			return new WP_Error( 'failed', sanitize_text_field( apply_filters( 'fast_events_new_order_text', esc_html__( 'Emailaddress is too long (Max 100 chars)', 'fast-events' ), 5 ) ), array( 'status' => 412 ) );
		}

		// Filter order emailaddress.
		$email = apply_filters( 'fast_events_order_mail', $email );
		if ( is_wp_error( $email ) ) {
			return $email;
		}

		// Filter order name.
		$name = apply_filters( 'fast_events_order_name', $name );
		if ( is_wp_error( $name ) ) {
			return $name;
		}

		// Fill id info for errors in the future.
		$this->id_info['name']  = $name;
		$this->id_info['email'] = $email;
		$this->id_info['ip']    = PluginUtil::get_ip();

		return array( $name, $email );
	}

	/**
	 * Check the Google or Cloudflare captcha
	 *
	 * @param array $params    Input fields from the form.
	 * @param int   $event_id  Unique id of the event.
	 * @return WP_Error|bool
	 *
	 * @since 2.3.0
	 */
	private function validate_captcha( array $params, int $event_id ): WP_Error|bool {
		$body = array( 'secret' => $this->settings->captcha_secret_key() );

		if ( 0 === $this->settings->captcha_provider() ) {
			$url              = self::FAST_EVENTS_CAPTCHA_GOOGLE;
			$body['response'] = $params['g-recaptcha-response'] ?? '';
		} else {
			$url              = self::FAST_EVENTS_CAPTCHA_CLOUDFLARE;
			$body['response'] = $params['cf-turnstile-response'] ?? '';
		}
		$response = wp_remote_post(
			$url,
			array(
				'method'  => 'POST',
				'headers' => array( 'Content-Type' => 'application/x-www-form-urlencoded' ),
				'body'    => http_build_query( $body ),
			)
		);

		// Network error?
		if ( is_wp_error( $response ) ) {
			$this->delayed_error = JsonUtil::fast_events_json_encode(
				array(
					'event_id'                  => $event_id,
					'order_id'                  => 0,
					'type'                      => 'CAPTCHA',
					'captcha_http_server_error' => $response->get_error_message(),
					'url'                       => $url,
					...$this->id_info,
				)
			);
			return new WP_Error( 'captcha_error', 'Captcha server error: ' . $response->get_error_message(), array( 'status' => 400 ) );
		}

		// Did we get an HTTP error?
		$http_code = (int) wp_remote_retrieve_response_code( $response );
		if ( 200 > $http_code || 299 < $http_code ) {
			$this->delayed_error = JsonUtil::fast_events_json_encode(
				array(
					'event_id'                => $event_id,
					'order_id'                => 0,
					'type'                    => 'CAPTCHA',
					/* translators: %d is the HTTP code */
					'error'                   => sprintf( esc_html__( 'Captcha server returned HTTP-code: %d', 'fast-events' ), $http_code ),
					'captcha_server_response' => json_decode( wp_remote_retrieve_body( $response ) ),
					'url'                     => $url,
					...$this->id_info,
				)
			);
			/* translators: %d is the HTTP code */
			return new WP_Error( 'captcha_error', sprintf( esc_html__( 'Captcha server returned HTTP-code: %d', 'fast-events' ), (int) wp_remote_retrieve_response_code( $response ) ), array( 'status' => 412 ) );
		}

		// Analyse the result.
		$result = json_decode( wp_remote_retrieve_body( $response ) );
		if ( null === $result || ! isset( $result->success ) || false === $result->success ) {
			$this->delayed_error = JsonUtil::fast_events_json_encode(
				array(
					'event_id'                => $event_id,
					'order_id'                => 0,
					'type'                    => 'CAPTCHA',
					/* translators: %d is the HTTP code */
					'error'                   => esc_html__( 'The CAPTCHA server responded with a failure', 'fast-events' ),
					'captcha_server_response' => $result,
					'url'                     => $url,
					...$this->id_info,
				)
			);
			return new WP_Error( 'captcha_failed', esc_html__( 'Captcha validation failed, please try again', 'fast-events' ), array( 'status' => 400 ) );
		}

		return true;
	}

	/**
	 * Check the event-form input
	 *
	 * @param WP_REST_Request $input          Input fields from the form.
	 * @param string          $payment_status Standard 'open', but can be 'dashboard'.
	 * @return WP_Error|array|bool|int|stdClass
	 *
	 * @since 1.0
	 * @since 1.7.1 Use wpdb->query and not wpdb->insert for a new order (Saves a db query (SHOW FULL COLUMNS...)
	 * @since 2.2.0 - Added filters 'fast_events_order_email' and 'fast_events_order_name'
	 *              - Refactor basic checks to new function.
	 *              - Add coupons.
	 * @since 2.3.0 - New captcha validation.
	 *              - Improved logging failed order insert.
	 */
	public function validate_form_input( WP_REST_Request $input, string $payment_status = 'open' ): WP_Error|array|bool|int|stdClass {
		global $wpdb;

		// Check the basics (name and email).
		$params = $input->get_params();
		$values = $this->validate_name_and_email( $params );
		if ( is_wp_error( $values ) ) {
			return $values;
		}
		$name        = $values[0];
		$email       = $values[1];
		$this->email = $email;

		// Check nonce, so we know nobody screwed up the fields.
		if ( 'dashboard' !== $payment_status ) {
			$nonce = 'event_nonce_value' . $params['event_info'];
			if ( ! wp_verify_nonce( $params['event_nonce'], $nonce ) ) {
				return new WP_Error( 'nonce_error', sanitize_text_field( apply_filters( 'fast_events_new_order_text', esc_html__( 'WordPress nonce error, please reload the orderform and try again', 'fast-events' ), 6 ) ), array( 'status' => 404 ) );
			}
		}

		$event_info        = json_decode( $params['event_info'] );
		$allowed_event_ids = array_map( fn( $value ) => intval( $value ), array_keys( (array) $event_info ) );
		$first_event_id    = (string) $allowed_event_ids[0];

		// Are there terms?
		if ( 1 === $event_info->{$first_event_id}->terms && 'dashboard' !== $payment_status ) {
			if ( ! isset( $params['fast-events-event-terms'] ) ) {
				return new WP_Error( 'no_terms', sanitize_text_field( apply_filters( 'fast_events_new_order_text', esc_html__( 'You must agree to the terms', 'fast-events' ), 7 ) ), array( 'status' => 404 ) );
			}
		}

		// Do we need to check a reCAPTCHA?
		if ( 'dashboard' !== $payment_status ) {
			if ( 1 === $event_info->{$first_event_id}->recaptcha ) {
				$resp = $this->validate_captcha( $params, $allowed_event_ids[0] );
				if ( is_wp_error( $resp ) ) {
					return $resp;
				}
			}
		}

		// Check different group types.
		switch ( $event_info->{$first_event_id}->group_type ) {
			case 0:
			case 3:
				$this->event_ids[] = $allowed_event_ids[0];
				break;
			case 1:  // Single select.
				if ( empty( $params['event_single'] ) ) {
					return new WP_Error( 'missing_param', sanitize_text_field( apply_filters( 'fast_events_new_order_text', esc_html__( 'Mandatory input not available', 'fast-events' ), 9 ) ), array( 'status' => 404 ) );
				}

				// Is there no selection?
				if ( 0 === (int) $params['event_single'] ) {
					return new WP_Error( 'missing_param', sanitize_text_field( apply_filters( 'fast_events_new_order_text', esc_html__( 'No event selected', 'fast-events' ), 10 ) ), array( 'status' => 404 ) );
				}

				// Is the selected event id correct; should not happen!
				if ( ! in_array( absint( $params['event_single'] ), $allowed_event_ids, true ) ) {
					return new WP_Error( 'no_event', sanitize_text_field( apply_filters( 'fast_events_new_order_text', esc_html__( 'Event does not exist', 'fast-events' ), 11 ) ), array( 'status' => 404 ) );
				}
				$this->event_ids[] = absint( $params['event_single'] );
				break;
			case 2:
				if ( empty( $params['event_multiple'] ) ) {
					return new WP_Error( 'missing_param', sanitize_text_field( apply_filters( 'fast_events_new_order_text', esc_html__( 'Mandatory input not available', 'fast-events' ), 9 ) ), array( 'status' => 404 ) );
				}

				// Check if the selected event_ids are valid.
				$this->event_ids = array_map( 'absint', $params['event_multiple'] );
				if ( ! empty( array_diff( $this->event_ids, $allowed_event_ids ) ) ) {
					return new WP_Error( 'not_allowed', sanitize_text_field( apply_filters( 'fast_events_new_order_text', esc_html__( 'Invalid event ids', 'fast-events' ), 12 ) ), array( 'status' => 400 ) );
				}
				break;
		}

		// Sort used events.
		sort( $this->event_ids, SORT_NUMERIC );

		// Loop through all the events.
		$event_table = $wpdb->prefix . 'fe_events';
		$order_table = $wpdb->prefix . 'fe_event_orders';
		foreach ( $this->event_ids as $single_id ) {

			// Get the event.
			$event = $wpdb->get_row( $wpdb->prepare( "SELECT * FROM $event_table WHERE id = %d FOR UPDATE", $single_id ) ); // phpcs:ignore
			if ( empty( $event ) ) {
				/* translators: %d is the event_id */
				return new WP_Error( 'no_event', sprintf( sanitize_text_field( apply_filters( 'fast_events_new_order_text', esc_html__( 'Event %d does not exist', 'fast-events' ), 14 ) ), $single_id ), array( 'status' => 404 ) );
			}
			$this->event_name                 = $event->event_name;
			$this->event_date                 = $event->event_date;
			$this->event_group                = $event->event_group;
			$this->saas_fee                   = is_null( $event->saas_fee ) ? null : floatval( $event->saas_fee );
			$this->saas_user_id               = absint( $event->saas_user_id );
			$this->event_test_payments_needed = (bool) $event->test_payments_needed;

			// Is the event still active? Should not happen, though, but just in case.
			if ( TimeUtil::local_time() < TimeUtil::date_local_time( $event->start_date ) || TimeUtil::local_time() > TimeUtil::date_local_time( $event->end_date ) ) {
				/* translators: %d is the event_id */
				return new WP_Error( 'no_event', sprintf( sanitize_text_field( apply_filters( 'fast_events_new_order_text', esc_html__( 'Event %d is not active', 'fast-events' ), 15 ) ), $single_id ), array( 'status' => 404 ) );
			}

			// Process the input fields.
			$json = $this->create_input_fields( $params, $event );
			if ( is_wp_error( $json ) ) {
				return $json;
			}

			// Check for coupons.
			$discount = 0.00;
			if ( 1 === (int) $event->coupons_needed && ! empty( $params[ 'coupon-' . $event->id ] ) ) {
				$discount = $this->check_coupon( $json, $params[ 'coupon-' . $event->id ], $event->id, $email, true );
				if ( is_wp_error( $discount ) ) {
					return $discount;
				}
			}

			// Check for unique users (Only single bookings/orders allowed).
			if ( $this->check_unique_users( $event, $email ) ) {
				return new WP_Error( 'failed', sanitize_text_field( apply_filters( 'fast_events_new_order_text', esc_html__( 'You already have a booking/order', 'fast-events' ), 13 ) ), array( 'status' => 412 ) );
			}

			// Filter the input. If a filter returns an error-string, we bail out.
			if ( has_filter( 'fast_events_input_fields' ) ) {
				/**
				 * Filter input fields and tickets.
				 *
				 * @param int    $event_id  The id of the event.
				 * @param string $name      The name of the person placing the order.
				 * @param string $email     The emailaddress of the person placing the order.
				 * @param string $total     The total order value, e.g. '6.50'.
				 * @param string $total_vat The total order VAT value.
				 * @param array  $fields    Array of input fields ((string)name and (string)value).
				 * @param array  $tickets   Array of ticket-types ordered ((string)name, (string)price, (string)vat and (int)count).
				 *
				 * @since 1.0
				 */
				$attr = apply_filters(
					'fast_events_input_fields',
					array(
						'id'        => $event->id,           // Read-only !
						'name'      => $name,
						'email'     => $email,               // Read-only !
						'total'     => $json->order_amount,  // Read-only !
						'total_vat' => $json->order_vat,     // Read-only !
						'fields'    => $json->fields,
						'tickets'   => $json->tickets,
					)
				);

				// Did a filter return an error?
				if ( isset( $attr['errors'] ) ) {
					return new WP_Error( 'input_error', esc_html( $attr['errors'] ), array( 'status' => 400 ) );
				}

				// Copy the result of the filter.
				$name          = esc_html( $attr['name'] );
				$json->fields  = $attr['fields'];
				$json->tickets = $attr['tickets'];

				// Just to be sure: sanitize output from filter.
				if ( false === $this->sanitize_input_and_ticket_fields( $json ) ) {
					return new WP_Error( 'filter_failed', sanitize_text_field( apply_filters( 'fast_events_new_order_text', esc_html__( 'Filter corrupted output', 'fast-events' ), 16 ) ), array( 'status' => 412 ) );
				}

				// Recalculate the totals (A filter may have changed values).
				$this->recalculate_ticket_totals( $json );
			}

			// Check for closed user groups.
			if ( $event->user_groups_needed ) {
				$result = $this->check_closed_user_group( $event, $json, $name, $email );
				if ( is_wp_error( $result ) ) {
					return $result;
				}

				// Can we book or order?
				if ( 0 === $result ) {
					return new WP_Error( 'failed', sanitize_text_field( apply_filters( 'fast_events_new_order_text', esc_html__( 'You are not allowed to book/order', 'fast-events' ), 17 ) ), array( 'status' => 412 ) );
				}

				// Are we allowed to book this many tickets?
				if ( $this->num_tickets > $result ) {
					/* translators: %d is the number of tickets */
					return new WP_Error( 'failed', sprintf( sanitize_text_field( apply_filters( 'fast_events_new_order_text', esc_html__( 'You can book a maximum of %d ticket(s)', 'fast-events' ), 18 ) ), $result ), array( 'status' => 412 ) );
				}
			}

			// Decrease stock.
			$stock_result = $this->update_available_stock( $params, $event );
			if ( is_wp_error( $stock_result ) ) {
				return $stock_result;
			} elseif ( false === $stock_result ) {
				/* translators: %s is the name of the event */
				return new WP_Error( 'failed', sprintf( sanitize_text_field( apply_filters( 'fast_events_new_order_text', esc_html__( 'No more stock available for "%s"', 'fast-events' ), 19 ) ), $event->event_name ), array( 'status' => 412 ) );
			}

			// Check if this is a passe-partout event and update the stock of the related child events.
			if ( 3 === $event_info->{$first_event_id}->group_type ) {
				$m_event = $this->update_passe_partout_child_stock( $event->event_group, $json );
				if ( is_wp_error( $m_event ) ) {
					return $m_event;
				}
				$this->event_ids = $m_event;
			}

			// Create a new order.
			$this->order_uid = UniqIds::uniq_order_id();
			$json->events    = $this->event_ids;
			$result          = $wpdb->query(
				$wpdb->prepare(
					'INSERT INTO ' . $order_table . // phpcs:ignore
					'(event_id,event_date,uid,payment_status,created_at,name,email,input_fields,num_tickets,total,discount,ip_address)' .
					' VALUES(%d,%s,%s,%s,%s,%s,%s,%s,%d,%f,%f,%s)',
					array(
						$event->id,
						$event->event_date,
						$this->order_uid,
						( 0.0 === $this->event_amount ) ? 'booked' : $payment_status,
						current_time( 'mysql' ),
						$name,
						$email,
						JsonUtil::fast_events_json_encode( $this->clean_password_fields( $json, $event ) ),
						$this->num_tickets,
						$this->event_amount,
						$discount,
						PluginUtil::get_ip(),
					)
				)
			);
			if ( false === $result ) {
				$this->delayed_error = JsonUtil::fast_events_json_encode(
					array(
						'event_id' => $event->id,
						'order_id' => 0,
						'type'     => 'REST API',
						'error'    => 'Order insert failed: ' . $wpdb->last_error,
						...$this->id_info,
					)
				);
				return new WP_Error( 'db_error', 'Order insert failed: ' . $wpdb->last_error, array( 'status' => 409 ) );
			}

			// Save the order-id.
			$this->order_ids[] = $wpdb->insert_id;

			// Save the input JSON structure per order.
			$this->json_per_event[ $wpdb->insert_id ] = $json;
		}

		// Update the array of input_fields with the order_ids.
		foreach ( $this->json_per_event as $key => $value ) {
			$this->json_per_event[ $key ]->orders = $this->order_ids;
		}

		return false;
	}


	/**
	 * Generate tickets for an array of orders
	 *
	 * @param array $orders Array of order id.
	 *
	 * @since 1.7.1 Use wpdb->query and not wpdb->insert (Saves a db query (SHOW FULL COLUMNS...)
	 *
	 * @since 1.0
	 */
	public function generate_tickets( array $orders ): void {
		global $wpdb;
		global $fevt_container;

		$qrcode_table = $wpdb->prefix . 'fe_event_qrcodes';
		$par_format   = '(%d,%d,%s,%s,%s,%f)';
		$cnt          = 0;

		do {
			$values       = array();
			$placeholders = array();
			$seats        = array();
			$query        = "INSERT INTO $qrcode_table (order_id,event_id,ticket_type,qrcode,created_at,amount) VALUES ";
			foreach ( $orders as $order ) {
				$fields = json_decode( $order->input_fields );
				if ( ! empty( $fields->tickets ) ) {
					foreach ( $fields->tickets as $ticket ) {
						for ( $i = 0; $i < $ticket->count; $i++ ) {
							$qrcode         = UniqIds::uniq_id();
							$placeholders[] = $par_format;
							array_push(
								$values,
								$order->id,
								$order->event_id,
								$ticket->name,
								$qrcode,
								current_time( 'mysql' ),
								$ticket->price,
							);

							// Is seating needed?
							if ( 1 === (int) $order->seats_needed ) {
								$tmp           = new stdClass();
								$tmp->event_id = $order->event_id;
								$tmp->qrcode   = $qrcode;
								$seats[]       = $tmp;
							}
						}
					}
				}
			}
			$query .= implode( ',', $placeholders );
			$result = $wpdb->query( $wpdb->prepare( $query, $values ) ); // phpcs:ignore

			// If for unknown reasons (likely a duplicate key on qrcode), the insert fails after 5 times, we give up.
			++$cnt;
		} while ( ! $result && $cnt < 6 );

		// Assign the seats.
		if ( ! empty( $seats ) ) {
			$fevt_container->get( SeatsHelper::class )->assign_seat_ids( $seats );
		}
	}


	/**
	 * Check the stock of the event or the linked event
	 *
	 * @param stdClass $event Event information.
	 * @return bool
	 *
	 * @since 1.0
	 */
	public function check_stock( stdClass $event ): bool {
		global $wpdb;

		// Check stock event.
		if ( 0 !== (int) $event->stock && (int) $event->sold >= (int) $event->stock ) {
			return false;
		}

		// Is the stock linked to another event?
		if ( 0 !== (int) $event->stock_link ) {
			$table_name = $wpdb->prefix . 'fe_events';
			$tmp_event  = $wpdb->get_row( $wpdb->prepare( "SELECT stock, sold FROM $table_name WHERE id = %d", absint( $event->stock_link ) ) ); // phpcs:ignore
			if ( empty( $tmp_event ) ) {  // Should not happen unless the user screwed up the input.
				return false;
			}

			// Check the stock of the linked event.
			if ( 0 !== (int) $tmp_event->stock && (int) $tmp_event->sold >= (int) $tmp_event->stock ) {
				return false;
			}
		}

		return true;
	}


	/**
	 * Count remaining tickets for the ticket type.
	 *
	 * @param stdClass $event  Event information.
	 * @param stdClass $ticket Ticket type information.
	 * @return int
	 *
	 * @since 2.4.0
	 */
	private function count_remaining_tickets( stdClass $event, stdClass $ticket ): int {
		global $wpdb;

		$rt = -1;

		// Is there stock control on the ticket?
		if ( '' !== $ticket->stock ) {
			$rt  = (int) $ticket->stock - (int) $ticket->sold;
			$tmp = (int) $event->stock - (int) $event->sold;
			if ( $tmp < $rt && 0 !== (int) $event->stock ) {
				$rt = $tmp;
			}
		} elseif ( 0 !== (int) $event->stock ) {
				$rt = (int) $event->stock - (int) $event->sold;
		}

		// Is there a linked event?
		if ( 0 !== (int) $event->stock_link ) {
			$link_event = $wpdb->get_row(
				$wpdb->prepare(
					"SELECT id, stock, sold, input_fields FROM {$wpdb->prefix}fe_events WHERE id = %d",
					absint( $event->stock_link )
				)
			);
			if ( empty( $link_event ) ) {
				return $rt;
			}

			// Match ticket types and check stock.
			$l_fields = json_decode( $link_event->input_fields );
			if ( ! empty( $l_fields->tickets ) ) {
				foreach ( $l_fields->tickets as $key => $value ) {

					// Does the ticket name match anyone sold?
					if ( $value->name === $ticket->name ) {

						// Is there stock control for the ticket type?
						if ( '' !== $value->stock ) {

							// Is there enough stock?
							$tmp = $value->stock - $value->sold;
							if ( -1 === $rt ) {
								$rt = $tmp;
							} elseif ( $tmp < $rt ) {
								$rt = $tmp;
							}
							return $rt;
						}
					}
				}
			}

			// Check the stock linked_event level only if it is not 0.
			if ( 0 !== (int) $link_event->stock ) {
				$tmp = (int) $link_event->stock - (int) $link_event->sold;
				if ( -1 === $rt ) {
					$rt = $tmp;
				} elseif ( $tmp < $rt ) {
					$rt = $tmp;
				}
			}
		}
		return $rt;
	}


	/**
	 * Generate ticket selection html
	 *
	 * @param stdClass $event   Event information.
	 * @param bool     $display Show the ticket table? (true or false).
	 *
	 * @since 1.0
	 * @since 1.4.0 Show sold out tickets with '0' select option.
	 * @since 2.2.0 - Added 'data-ticket-id' to select.
	 *              - Order input per event.
	 * @since 2.4.0 - Optionally show remaining tickets.
	 */
	private function generate_ticket_table( stdClass $event, bool $display = false ): void {
		$input_fields = JsonUtil::get_input_fields( $event->input_fields );
		if ( empty( $input_fields->tickets ) ) {
			return;
		}

		echo '<table class="fast-events-container" style="width: 100%; margin:5px 0 5px 0; ' . ( $display ? '' : 'display:none;' ) . '" ' .
			'id="fast-events-ticket-table-' . esc_html( $event->id ) . '">';

		// Output additional fields.
		if ( ! empty( $input_fields->fields ) ) {
			foreach ( $input_fields->fields as $field ) {
				if ( ! empty( $field->personalise ) && 1 === $field->personalise ) {
					continue; // This input field needs personalisation -> then we can't use it at order level.
				}
				echo '<tr><td colspan="3">';
				echo '<label for="' . esc_html( 'e' . $event->id . $field->id ) . '" class="fast-events-label"><b>' . esc_html( $field->name ) . '</b></label>';
				if ( 'select' === $field->type ) {
					$select_first = true;
					echo '<select class="fast-events-input" id="' . esc_html( 'e' . $event->id . $field->id ) .
					'" name="' . esc_html( 'e' . $event->id . $field->id ) . '" data-event-id="' . esc_html( $event->id ) . '"';
					if ( $field->required ) {
						echo ' required>';
					}
					foreach ( explode( ',', $field->value, 100 ) as $opt ) {
						echo '<option value="' . esc_html( $opt ) . ( $select_first ? '" selected>' : '">' ) . esc_html( $opt ) . '</option>';
						$select_first = false;
					}
					echo '</select>';
				} else {
					echo '<input class="fast-events-input" type="' . esc_html( $field->type ) . '" id="' . esc_html( 'e' . $event->id . $field->id ) .
						'" name="' . esc_html( 'e' . $event->id . $field->id ) . '" data-event-id="' . esc_html( $event->id ) . '"';
					if ( 'text' === $field->type || 'password' === $field->type ) {
						if ( ! empty( $field->min ) ) {
							echo ' minlength="' . esc_html( $field->min ) . '"';
						}
						if ( ! empty( $field->max ) ) {
							echo ' maxlength="' . esc_html( $field->max ) . '"';
						} else {
							echo ' maxlength="50"';
						}
					} else {
						if ( ! empty( $field->min ) ) {
							echo ' min="' . esc_html( $field->min ) . '"';
						}
						if ( ! empty( $field->max ) ) {
							echo ' max="' . esc_html( $field->max ) . '"';
						}
					}
					if ( $field->required ) {
						echo ' required';
					}
					if ( '' !== $field->value ) {
						echo ' value="' . esc_html( $field->value );
					}
					echo '">';
				}
				echo '</td></tr>';
			}
		}

		foreach ( $input_fields->tickets as $ticket ) {
			if ( '' !== $ticket->stock && (int) $ticket->sold >= (int) $ticket->stock ) {
				$ticket_desc     = '<b>' . esc_html( $ticket->name ) . '&nbsp;&nbsp;' . esc_html__( '(Sold out)', 'fast-events' ) . '</b>';
				$ticket_sold_out = true;
			} else {
				$ticket_desc = '<b>' . esc_html( $ticket->name ) . '</b>';
				if ( $ticket->remain ) { // Do we show the remaining tickets?
					$rt = $this->count_remaining_tickets( $event, $ticket );
					if ( $rt > 0 ) {
						/* translators: %s is the number of remaining tickets */
						$ticket_desc .= '<br><span><i>' . sprintf( _n( 'Only %s ticket left', 'Only %s tickets left', $rt, 'fast-events' ), number_format_i18n( $rt ) ) . '</i></span>';
					}
				}
				$ticket_sold_out = false;
			}

			// Check volume pricing.
			if ( ! empty( $ticket->volume_price ) ) {
				$ticket_desc  = '<div class="fast-events-collapsible">' . $ticket_desc . '</div>';
				$ticket_desc .= '<div class="fast-events-content">';
				usort( $ticket->volume_price, fn( $a, $b ) => (int) array_key_first( (array) $a ) - (int) array_key_first( (array) $b ) );
				foreach ( $ticket->volume_price as $vp ) {
					$obj = (array) $vp;
					$key = array_key_first( $obj );
					/* translators: %1$d is the number of tickets and %2$d is the amount */
					$ticket_desc .= '- ' . sprintf( __( 'From %1$s tickets for %2$s', 'fast-events' ), $key, '<b>' . $this->settings->currency_symbol() . ' ' . number_format_i18n( $obj[ $key ], 2 ) ) . '</b><br>';
				}
				$ticket_desc .= '</div>';
			}
			?>
			<tr><td class="fast-events-cell-padding" style="padding:3px 5px 3px 5px;">
			<?php
				echo $ticket_desc; // phpcs:ignore
			?>
			</td>
			<td class="fast-events-right-text fast-events-cell-padding" style="padding:3px 5px 3px 5px;white-space: nowrap"><?php echo esc_html( $this->settings->currency_symbol() ); ?>&nbsp;
			<?php echo esc_html( number_format_i18n( (float) $ticket->price, 2 ) ); ?></td>
			<td class="fast-events-right-text fast-events-cell-padding" style="padding:3px 5px 3px 5px;">
			<select dir="rtl" class="fast-events-event-tickets-select fast-events-event-select-event-<?php echo esc_html( $event->id ); ?>" data-price="<?php echo esc_html( $ticket->price ); ?>"
			data-ticket-id="<?php echo esc_html( $ticket->id ); ?>" data-event-id="<?php echo esc_html( $event->id ); ?>" name="<?php echo esc_html( 'e' . $event->id . $ticket->id ); ?>">
			<?php
			if ( ! $ticket_sold_out ) {
				for ( $i = $ticket->min; $i <= $ticket->max; $i++ ) {
					echo '<option value="' . esc_html( $i ) . '">' . esc_html( $i ) . '</option>';
				}
			} else {
				echo '<option value="0">0</option>';
			}

			echo '</select></td></tr>';
		}

		// Enter optional coupon code.
		if ( 1 === (int) $event->coupons_needed ) {
			echo '<tr style="margin-top:10px;"><td class="fast-events-coupon-block" style="padding:3px 5px 3px 5px;">' .
				'<div class="fast-events-coupon" data-event-id="' . esc_html( $event->id ) . '">' . esc_html__( 'Enter coupon code', 'fast-events' ) . '</div>' .
				'<div class="fast-events-coupon-entry" style="margin: 5px 0 5px 0;white-space: nowrap;display: none">' .
				'<input type="text" id="fast-events-coupon-code-' . esc_html( $event->id ) . '" name="coupon-' . esc_html( $event->id ) . '"' .
				' maxlength="32" placeholder="' . esc_html__( 'Discount code', 'fast-events' ) . '" />' .
				'&nbsp;<span id="fast-events-coupon-apply-' . esc_html( $event->id ) . '" style="margin: 5px 0 5px 0" class="fast-events-btn fast-events-success" data-event-id="' . esc_html( $event->id ) . '">' . esc_html__( 'Apply', 'fast-events' ) . '</span>' .
				'</div>' .
				'</td><td class="fast-events-right-text" style="padding:3px 5px 3px 5px;white-space: nowrap">' .
				'<div id="fast-events-coupon-amount-' . esc_html( $event->id ) . '"></div></td><td></td></tr>';
		}

		// Totals field.
		if ( 2 !== (int) $event->group_type ) {
			echo '<tr style="margin-top:10px;"><td style="padding:3px 5px 3px 5px;"><u><b>' . esc_html__( 'Total', 'fast-events' ) .
				'</b></u></td><td class="fast-events-right-text" style="padding:3px 5px 3px 5px;white-space: nowrap"><div class="fast-events-order-total">' . esc_html( $this->settings->currency_symbol() ) . ' ' .
				esc_html( number_format_i18n( '0.00', 2 ) ) . '</div></td><td></td></tr></table>';
		} else {
			echo '</table>';
		}
	}


	/**
	 * Generate the order form
	 *
	 * @param array $events Array of 1 or more events.
	 * @param int   $type   0=Client request, 1=Dashboard request -> no reCAPTCHA.
	 *
	 * @return false|string
	 * @since 1.0
	 * @since 1.3.0 Added translations to the order form
	 * @since 2.1.0 - Only include extra input fields in order form if they don't need personalisation
	 *              - Add additional types (date and time) and min/max values and minlength/maxlength.
	 * @since 2.2.0 - Added locale and coupons to the info structure.
	 *              - Added coupon entry in form.
	 * @since 2.3.0 - Use fast_events_json_encode.
	 *              - Added 'captcha_provider' (0 = Google, 1 = Cloudflare)
	 */
	public function get_order_form( array $events, int $type ): bool|string {

		// If a user is logged in, we show the name and emailaddress.
		$name  = '';
		$email = '';

		if ( is_user_logged_in() ) {
			$current_user = wp_get_current_user();
			$name         = $current_user->user_firstname . ' ' . $current_user->user_lastname;
			$email        = $current_user->user_email;
		}

		// Event info structure which is used for the nonce.
		$submit_string = __( 'Pay', 'fast-events' );
		$event_info    = array();
		foreach ( $events as $event ) {
			$input_fields = JsonUtil::get_input_fields( $event->input_fields );
			$extra_input  = array();
			foreach ( $input_fields->fields as $field ) {
				if ( 0 === $field->personalise ) { // Only the non-personalised input is allowed.
					$extra_input[] = $field;
				}
			}
			$ticket_types = array();
			foreach ( $input_fields->tickets as $ticket ) {
				$tmp                 = array();
				$tmp['id']           = $ticket->id;
				$tmp['price']        = $ticket->price;
				$tmp['volume_price'] = $ticket->volume_price ?? array();
				$tmp['min']          = $ticket->min;
				$tmp['max']          = $ticket->max;
				$ticket_types[]      = $tmp;
			}
			$event_info[ $event->id ] = array(
				'extra_input'  => $extra_input,
				'ticket_types' => $ticket_types,
				'terms'        => empty( $event->terms ) ? 0 : 1,
				'coupons'      => $event->coupons_needed ? 1 : 0,
				'recaptcha'    => $event->recaptcha_needed ? 1 : 0,
				'event_type'   => (int) $event->event_type,
				'group_type'   => (int) $event->group_type,
				'event_group'  => $event->event_group,
			);
			if ( ! empty( $input_fields->submit->name ) ) {
				$submit_string = $input_fields->submit->name;
			}
		}

		// Capture output.
		ob_start();

		?>
		<div class="fast-events-form-container">
		<form id="fast-events-form">

		<label for="fast-events-name" class="fast-events-label"><b><?php esc_html_e( 'Name', 'fast-events' ); ?></b></label>
		<input class="fast-events-input fast-events-common-field form-control" type="text" id="fast-events-name" name="name"
			value="<?php echo esc_html( $name ); ?>" required maxlength="50" autocomplete="on">

		<label for="fast-events-email" class="fast-events-label"><b><?php esc_html_e( 'Email', 'fast-events' ); ?></b></label>
		<input class="fast-events-input fast-events-common-field form-control" type="email" id="fast-events-email" name="email"
			value="<?php echo esc_html( $email ); ?>" required maxlength="100" autocomplete="on">

		<?php

		// Is there a group?
		if ( 1 < count( $events ) ) {
			if ( 1 === (int) $events[0]->group_type ) {  // Single selection from a group of events.
				echo '<label for="fast-events-single-input" class="fast-events-label"><b>' . esc_html( apply_filters( 'fast_events_new_order_text', esc_html__( 'Choose event', 'fast-events' ), 0 ) ) . '</b></label>';
				echo '<select id="fast-events-single-input" class="fast-events-single-select" name="event_single">';
				echo '<option value="0" selected>' . esc_html__( 'Your choice', 'fast-events' ) . '</option>';
				foreach ( $events as $item ) {

					// Check if there is enough stock.
					if ( true === $this->check_stock( $item ) && TimeUtil::local_time() > TimeUtil::date_local_time( $item->start_date ) && TimeUtil::local_time() < TimeUtil::date_local_time( $item->end_date ) ) {
						echo '<option value="' . esc_html( $item->id ) . '">' . esc_html( $item->event_name ) . '</option>';
					} else {
						echo '<option value="' . esc_html( $item->id ) . '" disabled>' . esc_html( $item->event_name ) . '&nbsp;&nbsp;' .
							esc_html__( '(Sold out)', 'fast-events' ) . '</option>';
					}
				}
				echo '</select>';

				// Output ticket tables.
				foreach ( $events as $event ) {
					$this->generate_ticket_table( $event );
				}
			} elseif ( 2 === (int) $events[0]->group_type ) { // Multiple selection of events.
				echo '<label for="fast-events-multiple" class="fast-events-label"><b>' . esc_html( apply_filters( 'fast_events_new_order_text', esc_html__( 'Choose 1 or more events', 'fast-events' ), 1 ) ) . '</b></label><br/>';
				foreach ( $events as $event ) {

					// Check if there is enough stock.
					if ( true === $this->check_stock( $event ) && TimeUtil::local_time() > TimeUtil::date_local_time( $event->start_date ) && TimeUtil::local_time() < TimeUtil::date_local_time( $event->end_date ) ) {
						echo '<input type="checkbox" class="fast-events-checkbox-group-type-2" data-event-id="' . esc_html( $event->id ) . '" name="event_multiple[]" value="' . esc_html( $event->id ) . '" />&nbsp;&nbsp;' . esc_html( $event->event_name ) . '<br/>';
					} else {
						echo '<input type="checkbox" class="fast-events-checkbox-group-type-2" data-event-id="' . esc_html( $event->id ) . '" name="event_multiple[]"  value="' . esc_html( $event->id ) . '" disabled />&nbsp;&nbsp;' .
							esc_html( $event->event_name ) . '&nbsp;&nbsp;' . esc_html__( '(Sold out)', 'fast-events' ) . '<br/>';
					}

					// Generate ticket table.
					$this->generate_ticket_table( $event );
				}

				// Output totals line.
				echo '<table style="table-layout:auto; margin:5px 0 5px 0;"><tr style="margin-top:10px;"><td><u><b>' .
					esc_html__( 'Total', 'fast-events' ) .
					'</b></u></td><td class="fast-events-right-text"><div class="fast-events-order-total">' .
					esc_html( $this->settings->currency_symbol() ) . ' ' .
					esc_html( number_format_i18n( '0.00', 2 ) ) . '</div></td></tr></table>';
			}
		} else {
			$this->generate_ticket_table( $events[0], true );
		}

		// Do we need a terms' checkbox?
		if ( ! empty( $events[0]->terms ) ) {
			echo '<div style="margin:15px 0 15px 0"><input type="checkbox" id="fast-events-event-terms" name="fast-events-event-terms" required>  &nbsp;' .
				'<label for="fast-events-event-terms">' .
				strip_tags( $events[0]->terms, array( 'a', 'b', 'i', 'u' ) ) . '</label></div>'; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
		}

		// Is reCAPTCHA needed?
		if ( self::FAST_EVENTS_CLIENT === $type ) {
			if ( $events[0]->recaptcha_needed ) {
				// Is it a Google reCaptcha?
				if ( 0 === $this->settings->captcha_provider() ) {
					wp_enqueue_script( 'ReCaptcha', 'https://www.google.com/recaptcha/api.js', null, fast_events_func()::FAST_EVENTS_VERSION, array( 'strategy' => 'defer' ) );
					echo '<div class="g-recaptcha" data-sitekey="' . esc_html( $this->settings->captcha_site_key() ) . '"></div>';
				} else { // It is a Cloudflare captcha.
					wp_enqueue_script( 'CloudflareCaptcha', 'https://challenges.cloudflare.com/turnstile/v0/api.js', null, fast_events_func()::FAST_EVENTS_VERSION, array( 'strategy' => 'defer' ) );
					echo '<div class="cf-turnstile" data-sitekey="' . esc_html( $this->settings->captcha_site_key() ) . '"></div>';
				}
			}
		}

		// Set the nonce and hidden fields.
		$event_info_string = JsonUtil::fast_events_json_encode( $event_info );
		$nonce             = 'event_nonce_value' . $event_info_string;
		$event_translation = JsonUtil::fast_events_json_encode(
			array(
				'endpoint'      => esc_url_raw( rest_url( '/fast-events/v1' ) ),
				'currency'      => $this->settings->currency_symbol(),
				'empty_field'   => esc_html__( 'Not all input fields have been completed', 'fast-events' ),
				'invalid_email' => esc_html__( 'Invalid emailaddress', 'fast-events' ),
				'check_terms'   => esc_html__( 'You must agree to the terms', 'fast-events' ),
				'no_tickets'    => esc_html__( 'No tickets ordered', 'fast-events' ),
				'timeout'       => esc_html__( 'The server did not respond within the given timeframe (10 seconds)', 'fast-events' ),
				'no_route'      => esc_html__( 'The server does not support this request; please ask the site administrator for help', 'fast-events' ),
				'fetch_failed'  => esc_html__( 'Could not connect to the server; please ask the site administrator for help', 'fast-events' ),
				'forbidden'     => esc_html__( 'You do not have permission on the server to execute this request; please ask the site administrator for help', 'fast-events' ),
				'invalid_json'  => esc_html__( 'The server did not return valid JSON; please ask the site administrator for help', 'fast-events' ),
			)
		)
		?>

		<input type="hidden" name="event_nonce" value="<?php echo esc_html( wp_create_nonce( $nonce ) ); ?>"/>
		<input type="hidden" id="event_info" name="event_info" value="<?php echo esc_html( $event_info_string ); ?>">
		<input type="hidden" id="event_translation" value="<?php echo esc_html( $event_translation ); ?>">

		<!-- Form submit -->
		<noscript><span style="color:red"><?php echo esc_html__( "You can't order tickets because your browser does not support JavaScript!", 'fast-events' ); ?></span><br></noscript>
		<input id="fast-events-form-submit" style="margin-top:10px;" class="btn btn-primary" type="submit" value="<?php echo esc_html( $submit_string ); ?>">
		</form>
		</div>
		<div id="fast-events-event-info"></div>

		<?php
		return ob_get_clean();
	}


	/**
	 * Create tickets and send email(s)
	 *
	 * @param array    $order_ids  List of order ids.
	 * @param array    $event_ids  List of event ids.
	 * @param stdClass $result     Order fields.
	 * @param int      $email_type Type of email (0=Normal, 1=Confirmation).
	 * @param string   $p_status   Payment status.
	 *
	 * @since 1.4.0 Added sender_name and sender_email to query
	 * @since 2.1.0 Added redirect to query
	 *
	 * @since 1.0
	 */
	public function fast_events_create_tickets_and_send_email( array $order_ids, array $event_ids, stdClass $result, int $email_type, string $p_status ): void {
		global $wpdb;
		global $fevt_container;

		$order_table = $wpdb->prefix . 'fe_event_orders';
		$event_table = $wpdb->prefix . 'fe_events';

		// Loop through all orders.
		foreach ( $order_ids as $order_id ) {
			if ( 3 === (int) $result->group_type ) {  // Passe-partout.
				$inp_fields = json_decode( $result->input_fields );
				$orders     = array();
				$cnt_events = count( $inp_fields->events );
				for ( $i = 0; $i < $cnt_events; $i++ ) {
					$orders[ $i ]                             = new stdClass();
					$orders[ $i ]->id                         = $inp_fields->orders[0];
					$orders[ $i ]->event_id                   = $inp_fields->events[ $i ];
					$orders[ $i ]->input_fields               = $result->input_fields;
					$orders[ $i ]->uid                        = $result->uid;
					$orders[ $i ]->name                       = $result->name;
					$orders[ $i ]->email                      = $result->email;
					$orders[ $i ]->created_at                 = $result->created_at;
					$orders[ $i ]->tickets_needed             = $result->tickets_needed;
					$orders[ $i ]->emails_needed              = $result->emails_needed;
					$orders[ $i ]->confirmation_emails_needed = $result->confirmation_emails_needed;
					$orders[ $i ]->confirmation_timeout       = $result->confirmation_timeout;
					$orders[ $i ]->seats_needed               = $result->seats_needed;
					$orders[ $i ]->webhooks_needed            = $result->webhooks_needed;
					$orders[ $i ]->tracking_needed            = $result->tracking_needed;
					$orders[ $i ]->sender_name                = $result->sender_name;
					$orders[ $i ]->sender_email               = $result->sender_email;
					$orders[ $i ]->email_bcc                  = $result->email_bcc;
					$orders[ $i ]->email_subject              = $result->email_subject;
					$orders[ $i ]->email_body                 = $result->email_body;
					$orders[ $i ]->confirm_bcc                = $result->confirm_bcc;
					$orders[ $i ]->confirm_subject            = $result->confirm_subject;
					$orders[ $i ]->confirm_body               = $result->confirm_body;
					$orders[ $i ]->redirect                   = $result->redirect;
				}
			} else {
				// phpcs:disable
				$orders = $wpdb->get_results(
					$wpdb->prepare(
						"SELECT $order_table.id, $order_table.uid, $order_table.payment_status, $order_table.event_id, " .
						"$order_table.payment_id, $order_table.input_fields, $order_table.name, $order_table.email, NOW() as created_at, " .
						"$event_table.sender_name, $event_table.sender_email, " .
						"$event_table.email_bcc, $event_table.email_subject, $event_table.email_body, " .
						"$event_table.confirm_bcc, $event_table.confirm_subject, $event_table.confirm_body, " .
						"$event_table.tickets_needed, $event_table.invoice_needed, $event_table.test_payments_needed, $event_table.webhooks_needed, " .
						"$event_table.emails_needed, $event_table.seats_needed, $event_table.confirmation_emails_needed, $event_table.confirmation_timeout, " .
						"$event_table.group_type, $event_table.event_group, $event_table.redirect " .
						"FROM $order_table " .
						"INNER JOIN $event_table ON $event_table.id = $order_table.event_id " .
						"WHERE $order_table.id = %d",
						$order_id
					)
				);
				// phpcs:enable
			}
			if ( empty( $orders ) ) {
				return;
			}

			// If needed, delete order delete action if the user confirms before timeout.
			if ( (bool) $orders[0]->confirmation_emails_needed && MailerInterface::FAST_EVENTS_CONFIRM_EMAIL === $email_type && 0 !== (int) $orders[0]->confirmation_timeout ) {
				as_unschedule_action(
					'fast_events_action_confirmation_timeout',
					array(
						'order_id' => (int) $orders[0]->id,
					),
					'fast-events-timeout-check'
				);
			}

			// Generate tickets.
			if ( (bool) $orders[0]->tickets_needed ) {
				if ( ! ( (bool) $orders[0]->confirmation_emails_needed && ( 'dashboard' === $p_status || 'booked' === $p_status ) ) ) {
					$this->generate_tickets( $orders );
				}
			}

			// If needed, schedule a future action to delete the order if the confirmation isn't coming in time.
			if ( (bool) $orders[0]->confirmation_emails_needed && MailerInterface::FAST_EVENTS_NORMAL_EMAIL === $email_type && 0 !== (int) $orders[0]->confirmation_timeout ) {
				as_schedule_single_action(
					TimeUtil::utc_time() + ( (int) $orders[0]->confirmation_timeout * MINUTE_IN_SECONDS ),
					'fast_events_action_confirmation_timeout',
					array(
						'order_id' => (int) $orders[0]->id,
					),
					'fast-events-timeout-check'
				);
			}

			// Send the email.
			if ( $orders[0]->emails_needed ) {
				try {
					$fevt_container->get( MailerFactory::class )->send_email( $orders[0], $email_type );
					// phpcs:ignore Generic.CodeAnalysis.EmptyStatement.DetectedCatch
				} catch ( Throwable $e ) {
					// Ignore the exception as we may be in background and error is already logged in the serverlog.
				}
			}

			// Fire webhooks if needed.
			if ( 1 === (int) $orders[0]->webhooks_needed ) {
				WebhookUtil::create_webhook( 'order.created', (int) $orders[0]->id );
			}

			// Action callback for a new order.
			if ( ! ( (bool) $orders[0]->confirmation_emails_needed && ( 'dashboard' === $p_status || 'booked' === $p_status ) ) ) {
				$json = json_decode( $orders[0]->input_fields, true );
				$attr = array(
					'order_ids' => $order_ids,
					'event_ids' => $event_ids,
					'name'      => $orders[0]->name,
					'email'     => $orders[0]->email,
					'total'     => $json['order_amount'],
					'total_vat' => $json['order_vat'] ?? '0.00',
					'fields'    => $json['fields'] ?? null,
					'tickets'   => $json['tickets'] ?? null,
				);

				/**
				 * New order action.
				 *
				 * @param array  $order_ids Array of order ids.
				 * @param array  $event_ids Array of event ids.
				 * @param string $name      The name of the person placing the order.
				 * @param string $email     The emailaddress of the person placing the order.
				 * @param string $total     The total order value, e.g. '6.50'.
				 * @param string $total_vat The total order VAT value.
				 * @param array  $fields    Array of input fields ((string)name and (string)value).
				 * @param array  $tickets   Array of ticket-types ordered ((string)name, (string)price, (string)vat and (int)count).
				 *
				 * @since 1.0
				 */
				do_action( 'fast_events_new_order', $attr );
			}
		}
	}


	/**
	 * Delete tickets after refund, chargebacks, failed, expired, or cancelled transactions
	 *
	 * @param array $event_ids       List of event_ids.
	 * @param array $order_ids       List of order ids.
	 *
	 * @since 1.0
	 */
	public function fast_events_delete_tickets( array $event_ids, array $order_ids ): void {
		global $wpdb;
		global $fevt_container;

		$event_table  = $wpdb->prefix . 'fe_events';
		$qrcode_table = $wpdb->prefix . 'fe_event_qrcodes';
		$entry_table  = $wpdb->prefix . 'fe_event_entries';

		// Loop through the events.
		foreach ( $event_ids as $event_id ) {

			$event = $wpdb->get_row(
				$wpdb->prepare(
					"SELECT seats_needed, tracking_needed, tracking_info FROM $event_table WHERE id = %d", // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
					absint( $event_id )
				)
			);
			if ( empty( $event ) ) {
				continue;
			}

			// Do we need to delete seats?
			if ( 1 === (int) $event->seats_needed ) {
				$qrcodes = $wpdb->get_col(
					"SELECT qrcode FROM $qrcode_table WHERE event_id=" . absint( $event_id ) . // phpcs:ignore
					' AND order_id IN (' . implode( ',', array_map( 'absint', $order_ids ) ) . ')' // phpcs:ignore
				);
				$fevt_container->get( SeatsHelper::class )->delete_seat_ids( (int) $event_id, $qrcodes );
			}
		}

		// Delete the tickets.
		$wpdb->query( "DELETE FROM $qrcode_table " . 'WHERE order_id IN (' . implode( ',', array_map( 'absint', $order_ids ) ) . ')' ); // phpcs:ignore

		// Delete scan entries (level 1 - 9).
		$wpdb->query( "DELETE FROM $entry_table WHERE qrcode_id IN (SELECT id FROM $qrcode_table WHERE order_id IN ( " . implode( ',', array_map( 'absint', $order_ids ) ) . ') )' ); // phpcs:ignore

		// Restock the tickets.
		$this->re_stock_tickets( $order_ids );
	}


	/**
	 * Duplicate an event
	 *
	 * @param WP_REST_Request $data Request instance.
	 * @return WP_Error|WP_REST_Response
	 *
	 * @since 1.10
	 * @since 2.3.0 Use fast_events_json_encode.
	 */
	public function fast_events_duplicate( WP_REST_Request $data ): WP_Error|WP_REST_Response {
		global $wpdb;
		global $fevt_container;

		// Check the event authorization.
		$resp = $fevt_container->get( AuthHelper::class )->check_event_authorization( absint( $data['id'] ) );
		if ( is_wp_error( $resp ) ) {
			return $resp;
		}

		// Look up the event.
		$event = $wpdb->get_row(
			$wpdb->prepare(
				"SELECT * FROM {$wpdb->prefix}fe_events WHERE id = %d",
				absint( $data['id'] )
			),
			ARRAY_A
		);

		if ( empty( $event ) ) {
			return new WP_Error( 'not_found', esc_html__( 'Event not found', 'fast-events' ), array( 'status' => 404 ) );
		}

		// Reset field and ticket ids, to prevent cross-event input.
		$cnt  = 0;
		$json = json_decode( $event['input_fields'] );
		foreach ( $json->fields as $key => $value ) {
			$json->fields[ $key ]->id = 'v' . $cnt . UniqIds::uniq_field();
			++$cnt;
		}
		foreach ( $json->tickets as $key => $value ) {
			$json->tickets[ $key ]->id   = 'v' . $cnt . UniqIds::uniq_field();
			$json->tickets[ $key ]->sold = 0;
			++$cnt;
		}
		$event['input_fields'] = JsonUtil::fast_events_json_encode( $json );

		// Insert the new row. First, remove the old 'id' and set sold on zero.
		unset( $event['id'] );
		$event['sold'] = 0;

		// Start the transaction.
		$wpdb->query( 'START TRANSACTION' );

		if ( ! $wpdb->insert( $wpdb->prefix . 'fe_events', $event ) ) {
			$wpdb->query( 'ROLLBACK' );
			/* translators: %s is the database error */
			return new WP_Error( 'insert_failed', sprintf( esc_html__( 'Duplicate failed: %s', 'fast-events' ), $wpdb->last_error ), array( 'status' => 409 ) );
		}
		$row_id = $wpdb->insert_id;

		// Insert the seats row for the new event.
		$resp = $fevt_container->get( SeatsHelper::class )->create_entry_for_event( $row_id, 0, '', '', 0 );
		if ( is_wp_error( $resp ) ) {
			$wpdb->query( 'ROLLBACK' );
			return $resp;
		}

		$wpdb->query( 'COMMIT' );

		// Fire webhooks if needed.
		if ( 1 === (int) $event['webhooks_needed'] ) {
			WebhookUtil::create_webhook( 'event.created', $row_id );
		}

		return rest_ensure_response( $row_id );
	}


	/**
	 * Remove orders and tickets from the event and delete event is '$delete_event' flag is set.
	 *
	 * @param WP_REST_Request $data         Request instance.
	 * @param bool            $delete_event Delete the event, true or false.
	 * @return WP_Error|string
	 *
	 * @since 1.0
	 * @since 1.4   Event authorization restriction
	 * @since 2.3.0 Use fast_events_json_encode.
	 * @since 2.5.0 Delete email log entries and log entries.
	 */
	public function fast_events_event_delete_helper( WP_REST_Request $data, bool $delete_event = false ): WP_Error|string {
		global $wpdb;
		global $fevt_container;

		$event_id = absint( $data['id'] );

		// Check the event authorization.
		$resp = $fevt_container->get( AuthHelper::class )->check_event_authorization( $event_id );
		if ( is_wp_error( $resp ) ) {
			return $resp;
		}

		// Get table names.
		$event_table  = $wpdb->prefix . 'fe_events';
		$qrcode_table = $wpdb->prefix . 'fe_event_qrcodes';
		$order_table  = $wpdb->prefix . 'fe_event_orders';
		$entry_table  = $wpdb->prefix . 'fe_event_entries';
		$email_table  = $wpdb->prefix . 'fe_event_email_log';
		$log_table    = $wpdb->prefix . 'fe_event_log';

		$event = $wpdb->get_row(
			$wpdb->prepare(
				"SELECT seats_needed, webhooks_needed FROM {$wpdb->prefix}fe_events WHERE id = %d",
				$event_id
			)
		);
		if ( ! $event ) {
			return new WP_Error( 'not_found', esc_html__( 'Event not found', 'fast-events' ), array( 'status' => 404 ) );
		}
		$webhooks_needed = (int) $event->webhooks_needed;

		// If seats are enabled, clear the seats. We do this for a possible seats linked_event.
		if ( 1 === (int) $event->seats_needed ) {
			$qrcodes = $wpdb->get_col( $wpdb->prepare( "SELECT qrcode FROM $qrcode_table WHERE event_id = %d", $event_id ) ); // phpcs:ignore
			$fevt_container->get( SeatsHelper::class )->delete_seat_ids( $event_id, $qrcodes );
		}

		// Start the transaction.
		$wpdb->query( 'SET autocommit=0' );

		// Lock tables. We use this to prevent a possible overflow of the internal lock table size as the potential # of orders and tickets can be high.
		$wpdb->query(
			"LOCK TABLES $event_table WRITE, $order_table WRITE, $qrcode_table WRITE, " . // phpcs:ignore
			"{$wpdb->prefix}fe_event_user_groups WRITE, $entry_table WRITE, {$wpdb->prefix}fe_event_seats WRITE, " . // phpcs:ignore
			"$email_table WRITE, $log_table WRITE" // phpcs:ignore
		);

		$event = $wpdb->get_row(
			$wpdb->prepare(
				"SELECT sold, stock_link, event_group, group_type, input_fields FROM {$wpdb->prefix}fe_events WHERE id = %d FOR UPDATE",
				$event_id
			)
		);
		if ( ! $event ) {
			$wpdb->query( 'ROLLBACK' );
			$wpdb->query( 'UNLOCK TABLES' );
			return new WP_Error( 'not_found', esc_html__( 'Event not found', 'fast-events' ), array( 'status' => 404 ) );
		}

		// If there is a linked event?
		$tmp_events = ( 0 !== (int) $event->stock_link ) ? array( (int) $event->stock_link ) : array();

		// Or is this a Passepartout event?
		if ( 3 === (int) $event->group_type ) {
			$result = $wpdb->get_results( // Get the associated events of the passe-partout.
				$wpdb->prepare(
					"SELECT id FROM {$wpdb->prefix}fe_events WHERE event_group = %s AND group_type = 2",
					$event->event_group
				)
			);
			if ( null !== $result ) {
				foreach ( $result as $row ) {
					$tmp_events[] = (int) $row->id;
				}
			}
		}

		// Walk through the events (Linked or PPT).
		$linked_events = array_unique( $tmp_events );
		$inp           = json_decode( $event->input_fields );
		$types_sold    = array();
		foreach ( $inp->tickets as $ticket ) {
			$types_sold[ $ticket->name ] = $ticket->sold;
		}
		foreach ( $linked_events as $linked_event_id ) {
			$linked_event = $wpdb->get_row(
				$wpdb->prepare(
					"SELECT sold, input_fields FROM {$wpdb->prefix}fe_events WHERE id = %d FOR UPDATE",
					$linked_event_id
				)
			);
			if ( ! $linked_event ) {
				continue;
			}

			// Subtract the sold tickets from the total sold tickets.
			$linked_fields = json_decode( $linked_event->input_fields );
			foreach ( $linked_fields->tickets as $key => $value ) {
				if ( isset( $types_sold[ $value->name ] ) ) {
					$linked_fields->tickets[ $key ]->sold -= $types_sold[ $value->name ];
					if ( 0 > $linked_fields->tickets[ $key ]->sold ) {
						$linked_fields->tickets[ $key ]->sold = 0;
					}
				}
			}

			// Update the linked or the PPT-related event.
			$wpdb->query(
				$wpdb->prepare(
					"UPDATE {$wpdb->prefix}fe_events SET input_fields = %s, sold = GREATEST(sold - %d, 0) WHERE id = %d",
					JsonUtil::fast_events_json_encode( $linked_fields ),
					(int) $event->sold,
					$linked_event_id
				)
			);
		}

		// Delete scan entries (level 1 - 9).
		$resp = $wpdb->query(
			$wpdb->prepare(
				"DELETE FROM $entry_table WHERE qrcode_id IN (SELECT id FROM $qrcode_table WHERE event_id=%d)", // phpcs:ignore
				$event_id
			)
		);
		if ( false === $resp ) {
			$wpdb->query( 'ROLLBACK' );
			$wpdb->query( 'UNLOCK TABLES' );
			return new WP_Error( 'not_found', esc_html__( 'Failed to delete ticket level scans', 'fast-events' ) . ': ' . $wpdb->last_error, array( 'status' => 400 ) );
		}

		// Delete associated tickets.
		$resp = $wpdb->delete( $qrcode_table, array( 'event_id' => $event_id ) );
		if ( false === $resp ) {
			$wpdb->query( 'ROLLBACK' );
			$wpdb->query( 'UNLOCK TABLES' );
			return new WP_Error( 'not_found', esc_html__( 'Failed to delete tickets', 'fast-events' ) . ': ' . $wpdb->last_error, array( 'status' => 400 ) );
		}

		// Delete associated orders.
		$resp = $wpdb->delete( $order_table, array( 'event_id' => $event_id ) );
		if ( false === $resp ) {
			$wpdb->query( 'ROLLBACK' );
			$wpdb->query( 'UNLOCK TABLES' );
			return new WP_Error( 'not_found', esc_html__( 'Failed to delete orders', 'fast-events' ) . ': ' . $wpdb->last_error, array( 'status' => 400 ) );
		}

		// Delete the email log entries.
		$resp = $wpdb->delete( $email_table, array( 'event_id' => $event_id ) );
		if ( false === $resp ) {
			$wpdb->query( 'ROLLBACK' );
			$wpdb->query( 'UNLOCK TABLES' );
			return new WP_Error( 'not_found', esc_html__( 'Failed to delete the order-email log', 'fast-events' ) . ': ' . $wpdb->last_error, array( 'status' => 400 ) );
		}

		// Delete the log entries.
		$resp = $wpdb->delete( $log_table, array( 'event_id' => $event_id ) );
		if ( false === $resp ) {
			$wpdb->query( 'ROLLBACK' );
			$wpdb->query( 'UNLOCK TABLES' );
			return new WP_Error( 'not_found', esc_html__( 'Failed to delete the order-log entries', 'fast-events' ) . ': ' . $wpdb->last_error, array( 'status' => 400 ) );
		}

		// Do we need to delete the event?
		if ( false === $delete_event ) {
			// Clear seats.
			$resp = $fevt_container->get( SeatsHelper::class )->clear_all_seats_for_event( $event_id );
			if ( is_wp_error( $resp ) ) {
				$wpdb->query( 'ROLLBACK' );
				$wpdb->query( 'UNLOCK TABLES' );
				return $resp;
			}

			// Reset sold ticket types to zero.
			$json = json_decode( $event->input_fields );
			foreach ( $json->tickets as $key => $value ) {
				$json->tickets[ $key ]->sold = 0;
			}

			// Set 'sold' to 0 for ticket types and event.
			$resp = $wpdb->update(
				$event_table,
				array(
					'input_fields' => JsonUtil::fast_events_json_encode( $json ),
					'sold'         => 0,
				),
				array( 'id' => $event_id )
			);
			if ( false === $resp ) {
				$wpdb->query( 'ROLLBACK' );
				$wpdb->query( 'UNLOCK TABLES' );

				return new WP_Error( 'not_found', esc_html__( 'Failed to update event', 'fast-events' ) . ': ' . $wpdb->last_error, array( 'status' => 400 ) );
			}

			/* translators: %d is the event number */
			$msg = sprintf( esc_html__( 'Event #%d: deleted all orders and tickets', 'fast-events' ), $event_id );
		} else {

			// Delete user groups.
			$resp = $wpdb->delete( $wpdb->prefix . 'fe_event_user_groups', array( 'event_id' => $event_id ) );
			if ( false === $resp ) {
				$wpdb->query( 'ROLLBACK' );
				$wpdb->query( 'UNLOCK TABLES' );
				return new WP_Error( 'not_found', esc_html__( 'Failed to delete user groups', 'fast-events' ) . ': ' . $wpdb->last_error, array( 'status' => 400 ) );
			}

			// Delete seats row.
			$resp = $fevt_container->get( SeatsHelper::class )->delete_entry_for_event( $event_id );
			if ( is_wp_error( $resp ) ) {
				$wpdb->query( 'ROLLBACK' );
				$wpdb->query( 'UNLOCK TABLES' );
				return $resp;
			}

			// Delete the event.
			$resp = $wpdb->delete( $event_table, array( 'id' => $event_id ) );
			if ( false === $resp ) {
				$wpdb->query( 'ROLLBACK' );
				$wpdb->query( 'UNLOCK TABLES' );

				return new WP_Error( 'not_found', esc_html__( 'Failed to delete event', 'fast-events' ) . ': ' . $wpdb->last_error, array( 'status' => 400 ) );
			}

			/* translators: %d is the event number */
			$msg = sprintf( esc_html__( 'Event #%d deleted', 'fast-events' ), $event_id );
		}

		// All went well: free resources.
		$wpdb->query( 'SET autocommit=1' );
		$wpdb->query( 'UNLOCK TABLES' );

		// Fire webhooks if needed.
		if ( true === $delete_event && 1 === $webhooks_needed ) {
			WebhookUtil::create_webhook( 'event.deleted', $event_id );
		}

		return $msg;
	}


	/**
	 * Check if provided id is an existing PDF template
	 *
	 * @param int $attachment_id The id of the PDF template.
	 *
	 * @return bool
	 * @since 1.0
	 */
	public function check_pdf_template_id( int $attachment_id ): bool {
		global $wpdb;

		$result = $wpdb->get_var(
			$wpdb->prepare(
				"SELECT COUNT(*) FROM $wpdb->posts WHERE post_type='attachment' AND post_mime_type='application/pdf' AND id=%d",
				$attachment_id
			)
		);
		if ( 0 === (int) $result ) {
			return false;
		} else {
			return true;
		}
	}

	/**
	 * Send HTML Email
	 * Can be used by action or filter functions to send email
	 *
	 * @param string $to        Emailadress.
	 * @param string $subject   Subject of the email.
	 * @param string $message   HTML formatted email.
	 *
	 * @since 1.0
	 */
	public function send_email( string $to, string $subject, string $message ): void {
		global $fevt_container;

		try {
			$fevt_container->get( MailerFactory::class )->send_html_message( $to, $subject, $message );
			// phpcs:ignore Generic.CodeAnalysis.EmptyStatement.DetectedCatch
		} catch ( Throwable $e ) {
			// Ignore the exception as we may be in background and error is already logged in the serverlog.
		}
	}
}
