<?php
/**
 * Order Helper controller.
 *
 * @since 1.0
 */

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

defined( 'ABSPATH' ) || exit;

use Exception;
use FeData\FastEvents\Logging\LoggerFactory;
use FeData\FastEvents\Mail\MailerFactory;
use FeData\FastEvents\Mail\MailerInterface;
use FeData\FastEvents\Payment\PaymentFactory;
use FeData\FastEvents\Storage\PluginSettings;
use FeData\FastEvents\Utilities\JsonUtil;
use FeData\FastEvents\Utilities\WebhookUtil;
use stdClass;
use WP_Error;
use WP_REST_Request;
use WP_REST_Response;



/**
 * Order Helper class.
 */
class OrderHelper {



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


	/**
	 * Get detailed information on scanned tickets
	 *
	 * @param array $event_ids Array of event identifiers.
	 *
	 * @return array Array of scans.
	 * @since 1.0
	 */
	public function order_scans( array $event_ids ): array {
		global $wpdb;

		// Look up the tickets that belong to the specified event and count and group them.
		$qrcode_table = $wpdb->prefix . 'fe_event_qrcodes';
		$entry_table  = $wpdb->prefix . 'fe_event_entries';

		// phpcs:disable
		return $wpdb->get_results(
			$wpdb->prepare(
				"SELECT ticket_type, 0 as scan_level, scan_location, COUNT(*) total FROM $qrcode_table " .
				'WHERE event_id in (' . implode( ',', $event_ids ) . ') ' .
				'GROUP BY ticket_type, scan_level, scan_location ' .
				'UNION ALL ' .
				"SELECT $qrcode_table.ticket_type, $entry_table.scan_level, " .
				"$entry_table.scan_location, COUNT(*) total FROM $qrcode_table " .
				"INNER JOIN $entry_table ON $entry_table.qrcode_id = $qrcode_table.id " .
				"WHERE $qrcode_table.event_id in (" . implode( ',', $event_ids ) . ') ' .
				'GROUP BY ticket_type, scan_level, scan_location ' .
				'ORDER BY ticket_type ASC, scan_level ASC, scan_location ASC',
			)
		);
		// phpcs:enable
	}


	/**
	 * Send detailed order information
	 *
	 * @param int $order_id Order identifier.
	 *
	 * @return stdClass|WP_Error
	 * @since 2.2.0 Added event_date and group_type
	 *
	 * @since 1.0
	 * @since 1.4.0 Event authorization restriction
	 */
	public function order_details( int $order_id ): WP_Error|stdClass {
		global $wpdb;

		// Look up the order.
		$order_table = $wpdb->prefix . 'fe_event_orders';
		$event_table = $wpdb->prefix . 'fe_events';

		// phpcs:disable
		$result = $wpdb->get_row(
			$wpdb->prepare(
				"SELECT $order_table.event_id, $order_table.payment_id, $order_table.uid, " .
				"$order_table.payment_status, $order_table.custom_status, $order_table.input_fields, " .
				"$order_table.name, $order_table.email, $order_table.created_at, " .
				"$order_table.num_tickets, $order_table.total, $event_table.event_name, $event_table.event_date, " .
				"$event_table.group_type, $event_table.saas_user_id, $event_table.test_payments_needed " .
				"FROM $order_table " .
				"INNER JOIN $event_table ON $event_table.id = $order_table.event_id " .
				"WHERE $order_table.id = %d" .
				$this->auth->get_restricted_events( $this->auth::FAST_EVENTS_AUTH_QUERY_ORDERS, $event_table ),
				$order_id
			)
		);
		// phpcs:enable

		// Does the key exist?
		if ( empty( $result ) ) {
			return new WP_Error( 'not_found', esc_html__( 'No order found', 'fast-events' ), array( 'status' => 404 ) );
		}

		// Create the basic structure.
		$resp                = new stdClass();
		$resp->id            = strval( $order_id );
		$resp->uid           = $result->uid;
		$resp->event_id      = $result->event_id;
		$resp->event_name    = $result->event_name;
		$resp->event_date    = $result->event_date;
		$resp->group_type    = $result->group_type;
		$resp->custom_status = $result->custom_status;
		$resp->name          = $result->name;
		$resp->email         = $result->email;
		$resp->status        = $result->payment_status;
		$resp->num_tickets   = $result->num_tickets;
		$resp->total         = $result->total;
		$resp->created_at    = $result->created_at;
		$resp->input_fields  = $result->input_fields;
		if ( ! empty( $result->payment_id ) ) {
			$resp->payment_id = $result->payment_id;
		}

		return $resp;
	}


	/**
	 * Send order email
	 *
	 * @param int    $id Order identifier.
	 * @param string $email Emailaddress.
	 *
	 * @return string|WP_Error
	 * @since 1.0
	 * @since 1.4.0 - Event authorization restriction
	 *              - Added sender_name and sender_email to query
	 * @since 2.1.0 - Added redirect to query
	 */
	public function order_email( int $id, string $email ): WP_Error|string {
		global $wpdb;
		global $fevt_container;

		// Look up the order.
		$order_table = $wpdb->prefix . 'fe_event_orders';
		$event_table = $wpdb->prefix . 'fe_events';

		// phpcs:disable
		$result = $wpdb->get_row(
			$wpdb->prepare(
				"SELECT $order_table.id, $order_table.uid, $order_table.payment_status, $order_table.event_id, " .
				"$order_table.input_fields, $order_table.name, $order_table.email, NOW() as created_at, " .
				"$event_table.email_bcc, $event_table.emails_needed, $event_table.sender_name, " .
				"$event_table.sender_email, $event_table.email_subject, " .
				"$event_table.email_body, $event_table.confirm_bcc, $event_table.confirm_subject, " .
				"$event_table.confirm_body, $event_table.redirect " .
				"FROM $order_table " .
				"INNER JOIN $event_table ON $event_table.id = $order_table.event_id " .
				"WHERE $order_table.id = %d" .
				$this->auth->get_restricted_events( $this->auth::FAST_EVENTS_AUTH_QUERY_ORDERS, $event_table ),
				$id
			)
		);
		// phpcs:enable

		// Does the key exist?
		if ( null === $result ) {
			return new WP_Error( 'not_found', esc_html__( 'No order found', 'fast-events' ), array( 'status' => 404 ) );
		}

		// Is the order paid?
		if ( 'paid' !== $result->payment_status && 'dashboard' !== $result->payment_status && 'booked' !== $result->payment_status &&
			'booked - confirmed' !== $result->payment_status && 'dashboard - confirmed' !== $result->payment_status ) {
			return new WP_Error( 'status_error', esc_html__( 'Order has wrong status', 'fast-events' ), array( 'status' => 404 ) );
		}

		// Do we need to send an email?
		if ( $result->emails_needed ) {

			// Temp set the emailaddress to the given one.
			$result->email = $email;

			// Send the email.
			try {
				$fevt_container->get( MailerFactory::class )->send_email( $result );
			} catch ( Exception $e ) {
				return new WP_Error( 'email_error', $e->getMessage(), array( 'status' => 500 ) );
			}
		} else {
			return new WP_Error( 'email_error', esc_html__( 'Emailing is not set up for this event', 'fast-events' ), array( 'status' => 404 ) );
		}

		/* translators: %d is the order id and %s is the emailaddress */

		return sprintf( esc_html__( 'Order #%1$d: Email send to "%2$s"', 'fast-events' ), $result->id, $email );
	}


	/**
	 * Delete order and possible qrcodes
	 *
	 * @param int $id Order identifier.
	 *
	 * @return string|WP_Error
	 * @since 2.5.0 Delete email log entries and log entries.
	 *
	 * @since 1.0
	 * @since 1.4   Event authorization restriction
	 */
	public function order_delete( int $id ): WP_Error|string {
		global $wpdb;
		global $fevt_container;
		static $pstatus = array(
			'open',
			'paid',
			'processing',
			'booked',
			'booked - confirmed',
			'dashboard',
			'dashboard - confirmed',
		);

		// Look up the order.
		$order_table  = $wpdb->prefix . 'fe_event_orders';
		$event_table  = $wpdb->prefix . 'fe_events';
		$email_table  = $wpdb->prefix . 'fe_event_email_log';
		$log_table    = $wpdb->prefix . 'fe_event_log';
		$qrcode_table = $wpdb->prefix . 'fe_event_qrcodes';
		// phpcs:disable
		$order        = $wpdb->get_row(
			$wpdb->prepare(
				"SELECT $order_table.id, $order_table.event_id, $order_table.payment_status, $event_table.seats_needed, 
				$event_table.webhooks_needed, $event_table.tracking_needed, $event_table.tracking_info " .
				"FROM $order_table " .
				"INNER JOIN $event_table ON $event_table.id = $order_table.event_id " .
				"WHERE $order_table.id = %d" .
				$this->auth->get_restricted_events( $this->auth::FAST_EVENTS_AUTH_QUERY_ORDERS, $event_table ),
				$id
			)
		);
		// phpcs:enable
		if ( empty( $order ) ) {
			return new WP_Error( 'not_found', esc_html__( 'No order found', 'fast-events' ), array( 'status' => 404 ) );
		}

		// Restock the tickets if they have the right payment_status.
		if ( in_array( $order->payment_status, $pstatus, true ) ) {
			$this->events->re_stock_tickets( array( $id ) );
		}

		// Were there seats reserved?
		if ( 1 === (int) $order->seats_needed ) {
			$qrcodes = $wpdb->get_col( $wpdb->prepare( "SELECT qrcode FROM {$wpdb->prefix}fe_event_qrcodes WHERE order_id = %d", $id ) );
			$fevt_container->get( SeatsHelper::class )->delete_seat_ids( (int) $order->event_id, $qrcodes );
		}

		// Delete scan entries (level 1 and 9).
		$wpdb->query(
			$wpdb->prepare(
				"DELETE FROM {$wpdb->prefix}fe_event_entries " .
				"WHERE qrcode_id IN (SELECT id FROM {$wpdb->prefix}fe_event_qrcodes WHERE order_id=%d)",
				$id
			)
		);

		// Delete the Qrcodes.
		$wpdb->delete( $qrcode_table, array( 'order_id' => $id ) );

		// Delete the email log entries.
		$wpdb->delete( $email_table, array( 'order_id' => $id ) );

		// Delete the log entries.
		$wpdb->delete( $log_table, array( 'order_id' => $id ) );

		// Delete the order.
		$wpdb->delete( $order_table, array( 'id' => $id ) );

		// If something is deleted -> fire the webhook.
		if ( 1 === (int) $order->webhooks_needed ) {
			WebhookUtil::create_webhook( 'order.deleted', $id );
		}

		return esc_html__( 'Deleted order #', 'fast-events' ) . $id;
	}


	/**
	 * Delete qrcodes
	 *
	 * @param int $id Order identifier.
	 * @return string|WP_Error
	 *
	 * @since 1.0
	 * @since 1.4 Event authorization restriction
	 */
	public function qrcode_delete( int $id ): WP_Error|string {
		global $wpdb;
		global $fevt_container;

		// Look up the order.
		$order = $wpdb->get_row( $wpdb->prepare( "SELECT event_id FROM {$wpdb->prefix}fe_event_orders WHERE id = %d", $id ) );
		if ( empty( $order ) ) {
			return new WP_Error( 'not_found', esc_html__( 'No order found', 'fast-events' ), array( 'status' => 404 ) );
		}

		// Check the event authorization.
		$resp = $this->auth->check_event_authorization( (int) $order->event_id );
		if ( is_wp_error( $resp ) ) {
			return $resp;
		}

		// Get the event.
		$event = $wpdb->get_row(
			$wpdb->prepare(
				"SELECT tickets_needed, seats_needed, tracking_needed, tracking_info from {$wpdb->prefix}fe_events WHERE id=%d",
				$order->event_id
			)
		);

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

		// Are there qrcodes for this event?
		if ( ! $event->tickets_needed ) {
			return new WP_Error( 'not_found', esc_html__( 'This event is not using tickets', 'fast-events' ), array( 'status' => 404 ) );
		}

		// Were there seats reserved?
		if ( 1 === (int) $event->seats_needed ) {
			$qrcodes = $wpdb->get_col( $wpdb->prepare( "SELECT qrcode FROM {$wpdb->prefix}fe_event_qrcodes WHERE order_id = %d", $id ) );
			$fevt_container->get( SeatsHelper::class )->delete_seat_ids( (int) $order->event_id, $qrcodes );
		}

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

		// Delete the Qrcodes.
		$resp = $wpdb->delete( $wpdb->prefix . 'fe_event_qrcodes', array( 'order_id' => $id ) );
		if ( false === $resp ) {
			return new WP_Error( 'not_found', esc_html__( 'Failed to delete tickets', 'fast-events' ) . ': ' . $wpdb->last_error, array( 'status' => 400 ) );
		}

		return esc_html__( 'Deleted all tickets of order #', 'fast-events' ) . $id;
	}


	/**
	 * Create tickets (qrcodes)
	 *
	 * @param int $order_id Order identifier.
	 * @return string|WP_Error
	 *
	 * @since 1.0
	 */
	public function qrcode_create( int $order_id ): WP_Error|string {
		global $wpdb;

		// Look up the order.
		$order_table = $wpdb->prefix . 'fe_event_orders';
		$order       = $wpdb->get_row(
			$wpdb->prepare(
				"SELECT event_id, payment_status, input_fields, name, email FROM {$wpdb->prefix}fe_event_orders WHERE id = %d",
				$order_id
			)
		);
		if ( empty( $order ) ) {
			return new WP_Error( 'not_found', esc_html__( 'No order found', 'fast-events' ), array( 'status' => 404 ) );
		}

		// Check the event authorization.
		$resp = $this->auth->check_event_authorization( (int) $order->event_id );
		if ( is_wp_error( $resp ) ) {
			return $resp;
		}

		// Get the event.
		$event_table = $wpdb->prefix . 'fe_events';
		$event       = $wpdb->get_row(
			$wpdb->prepare(
				"SELECT group_type, event_group, tickets_needed, seats_needed, tracking_needed from {$wpdb->prefix}fe_events WHERE id=%d",
				(int) $order->event_id
			)
		);
		if ( empty( $event ) ) {
			return new WP_Error( 'not_found', esc_html__( 'Event not found', 'fast-events' ), array( 'status' => 404 ) );
		}

		// Are tickets needed for this event?
		if ( ! $event->tickets_needed ) {
			return new WP_Error( 'not_found', esc_html__( 'This event is not using tickets', 'fast-events' ), array( 'status' => 404 ) );
		}

		// Is the order paid?
		if ( ! in_array(
			$order->payment_status,
			array(
				'paid',
				'dashboard',
				'dashboard - confirmed',
				'booked',
				'booked - confirmed',
			),
			true
		) ) {
			return new WP_Error( 'refund_error', esc_html__( 'Order has wrong payment status for tickets', 'fast-events' ), array( 'status' => 400 ) );
		}

		// Are there already tickets for this order?
		$tickets = $wpdb->get_var( $wpdb->prepare( "SELECT COUNT(*) FROM {$wpdb->prefix}fe_event_qrcodes WHERE order_id = %d", $order_id ) );
		if ( null === $tickets || 0 === (int) $tickets ) {
			$json = json_decode( $order->input_fields );
			if ( 3 === (int) $event->group_type ) {  // Passe-partout.
				$orders    = array();
				$cnt_event = count( $json->events );
				for ( $i = 0; $i < $cnt_event; $i++ ) {
					$orders[ $i ]                  = new stdClass();
					$orders[ $i ]->id              = $order_id;
					$orders[ $i ]->event_id        = $json->events[ $i ];
					$orders[ $i ]->input_fields    = $order->input_fields;
					$orders[ $i ]->name            = $order->name;
					$orders[ $i ]->email           = $order->email;
					$orders[ $i ]->seats_needed    = $event->seats_needed;
					$orders[ $i ]->tracking_needed = $event->tracking_needed;
				}
			} else {
				// phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared
				$orders = $wpdb->get_results(
					$wpdb->prepare(
						"SELECT $order_table.id, $order_table.event_id, $order_table.input_fields, " .
						"$order_table.name, $order_table.email, $event_table.seats_needed, $event_table.tracking_needed  " .
						"FROM $order_table " .
						"INNER JOIN $event_table ON $event_table.id = $order_table.event_id " .
						"WHERE $order_table.id=%d ",
						$order_id
					)
				);
				// phpcs:enable WordPress.DB.PreparedSQL.InterpolatedNotPrepared
			}
			if ( empty( $orders ) ) {
				return new WP_Error( 'not_found', esc_html__( 'No order(s) found', 'fast-events' ), array( 'status' => 404 ) );
			}

			// Generate tickets.
			$this->events->generate_tickets( $orders );

			return esc_html__( 'Created tickets for order #', 'fast-events' ) . $order_id;
		} else {
			return new WP_Error( 'duplicate', esc_html__( 'This order already has tickets', 'fast-events' ), array( 'status' => 400 ) );
		}
	}


	/**
	 * Get qrcodes and checkin info belonging to order
	 *
	 * @param int $order_id Order identifier.
	 *
	 * @return array|object|null
	 * @since  2.1.0 Add 'personalised_input' to the query
	 * @since  2.2.0 Add 'event_name', 'event_date' and 'group_type' to the query
	 * @since  2.4.0 Sorting: date has priority over level.
	 *
	 * @since  1.0
	 * @since  1.4.0 Event authorization restriction
	 */
	public function get_qrcodes( int $order_id ): object|array|null {
		global $wpdb;
		global $fevt_container;

		// Look up the order.
		$event_table  = $wpdb->prefix . 'fe_events';
		$qrcode_table = $wpdb->prefix . 'fe_event_qrcodes';
		$entry_table  = $wpdb->prefix . 'fe_event_entries';

		// phpcs:disable
		$result = $wpdb->get_results(
			$wpdb->prepare(
				"SELECT $qrcode_table.id, $qrcode_table.ticket_type, $qrcode_table.event_id, 0 as scan_level, $qrcode_table.qrcode, " .
				"$qrcode_table.scan_date, $qrcode_table.scan_location, $qrcode_table.personalised_input, $event_table.seats_needed, " .
				"$event_table.event_name, $event_table.event_date, $event_table.group_type " .
				"FROM $qrcode_table " .
				"INNER JOIN $event_table ON $event_table.id = $qrcode_table.event_id " .
				"WHERE $qrcode_table.order_id = %d" .
				$this->auth->get_restricted_events( $this->auth::FAST_EVENTS_AUTH_QUERY_ORDERS, $event_table ) .
				' UNION ' .
				"SELECT $entry_table.id, $qrcode_table.ticket_type, $qrcode_table.event_id, $entry_table.scan_level, " .
				"$qrcode_table.qrcode, $entry_table.scan_date, $entry_table.scan_location, $qrcode_table.personalised_input, $event_table.seats_needed, " .
				"$event_table.event_name, $event_table.event_date, $event_table.group_type " .
				"FROM $qrcode_table " .
				"INNER JOIN $entry_table ON $entry_table.qrcode_id = $qrcode_table.id " .
				"INNER JOIN $event_table ON $event_table.id = $qrcode_table.event_id " .
				"WHERE $qrcode_table.order_id = %d" .
				$this->auth->get_restricted_events( $this->auth::FAST_EVENTS_AUTH_QUERY_ORDERS, $event_table ) .
				' ORDER BY ticket_type, qrcode, scan_date, scan_level',
				$order_id,
				$order_id
			)
		);
		// phpcs:enable

		// Check if seats are used and add them if needed.
		foreach ( $result as $key => $value ) {
			if ( 1 === (int) $value->seats_needed ) {
				$result[ $key ]->ticket_type .= $fevt_container->get( SeatsHelper::class )->get_seat( (int) $value->event_id, $value->qrcode );
			}
		}

		return $result;
	}


	/**
	 * Process checkin request for order
	 *
	 * @param WP_REST_Request $data Request instance.
	 * @return WP_Error|WP_REST_Response
	 *
	 * @since  1.10
	 */
	public function process_checkin( WP_REST_Request $data ): WP_Error|WP_REST_Response {
		global $wpdb;

		$params   = $data->get_params();
		$order_id = $params['orderid'];

		$webhook_scans_created = array();

		// Look up the order.
		$event_table  = $wpdb->prefix . 'fe_events';
		$qrcode_table = $wpdb->prefix . 'fe_event_qrcodes';
		$entry_table  = $wpdb->prefix . 'fe_event_entries';

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

		// phpcs:disable
		$tickets = $wpdb->get_results(
			$wpdb->prepare(
				"SELECT $qrcode_table.id, $qrcode_table.scan_date, $event_table.webhooks_needed FROM $qrcode_table " .
				"INNER JOIN $event_table ON $event_table.id = $qrcode_table.event_id " .
				" WHERE $qrcode_table.order_id = %d" .
				$this->auth->get_restricted_events( $this->auth::FAST_EVENTS_AUTH_QUERY_ORDERS, $event_table ) .
				' FOR UPDATE',
				$order_id
			)
		);
		// phpcs:enable
		if ( empty( $tickets ) ) {
			$wpdb->query( 'ROLLBACK' );

			return new WP_Error( 'not_found', esc_html__( 'No tickets found', 'fast-events' ), array( 'status' => 404 ) );
		}

		// Walk through all the tickets and update (code = 0 -> Main entrance).
		foreach ( $tickets as $ticket ) {
			$param = 'qrcode-0-' . $ticket->id;
			if ( isset( $params[ $param ] ) && 'on' === $params[ $param ] ) {
				// Only need to update if it's not on yet.
				if ( null === $ticket->scan_date ) {
					$wpdb->update(
						$qrcode_table,
						array(
							'scan_date'     => current_time( 'mysql' ),
							'scan_location' => 'Dashboard',
						),
						array( 'id' => $ticket->id )
					);
					$webhook_scans_created[] = array( (int) $ticket->id, 0, 0 );
				}
			} else {
				// Only need to update if it's not off yet.
				if ( null !== $ticket->scan_date ) {
					$wpdb->update(
						$qrcode_table,
						array(
							'scan_date'     => null,
							'scan_location' => '',
							'exit_scan'     => 0,
						),
						array( 'id' => $ticket->id )
					);
				}
				// Remove staged scans.
				$wpdb->delete( $entry_table, array( 'qrcode_id' => $ticket->id ) );
			}
		}

		// Walk through all staged scans (code = 1 - 9).
		// phpcs:disable
		$entries = $wpdb->get_results(
			$wpdb->prepare(
				"SELECT $entry_table.id,  $qrcode_table.order_id, " .
				"$entry_table.qrcode_id, $entry_table.scan_level FROM $qrcode_table " .
				"INNER JOIN $entry_table ON $entry_table.qrcode_id = $qrcode_table.id " .
				"WHERE $qrcode_table.order_id = %d FOR UPDATE",
				$order_id
			)
		);
		// phpcs:enable
		if ( ! empty( $entries ) ) {
			foreach ( $entries as $entry ) {
				// Check level 1 - 9.
				$param = 'qrcode-' . $entry->scan_level . '-' . $entry->id;
				if ( ! isset( $params[ $param ] ) ) {
					$wpdb->delete( $entry_table, array( 'id' => $entry->id ) );
					if ( 9 === (int) $entry->scan_level ) {
						$wpdb->update( $qrcode_table, array( 'exit_scan' => 0 ), array( 'id' => $entry->qrcode_id ) );
					}
				}
			}
		}
		$wpdb->query( 'COMMIT' );

		// If there were scans -> fire webhook. We don't flag deleted scans!
		if ( 1 === (int) $tickets[0]->webhooks_needed ) {
			foreach ( $webhook_scans_created as $update ) {
				WebhookUtil::create_webhook( 'scan.created', $update );
			}
		}

		/* translators: %d is the unique order id */

		return rest_ensure_response( sprintf( esc_html__( 'Checkin processed for order #%d', 'fast-events' ), $order_id ) );
	}

	/**
	 * Refund order
	 *
	 * @param int $order_id Order identifier.
	 *
	 * @return string|WP_Error
	 * @since 2.4.0 Get User-Agent from utility
	 *
	 * @since 1.0
	 * @since 1.4   Event authorization restriction
	 */
	public function order_refund( int $order_id ): WP_Error|string {
		global $wpdb;
		global $fevt_container;

		// Look up the order.
		$order_table = $wpdb->prefix . 'fe_event_orders';
		// phpcs:disable
		$order       = $wpdb->get_row(
			$wpdb->prepare(
				"SELECT id, uid, event_id, payment_status, payment_id, input_fields, total, num_tickets FROM $order_table WHERE id = %d" .
				$this->auth->get_restricted_events( $this->auth::FAST_EVENTS_AUTH_QUERY_ORDER_LOOKUP ),
				$order_id
			)
		);
		// phpcs:enable
		if ( empty( $order ) ) {
			return new WP_Error( 'not_found', esc_html__( 'No order found', 'fast-events' ), array( 'status' => 404 ) );
		}

		// Is it already refunded?
		if ( 'refunded' === $order->payment_status ) {
			return new WP_Error( 'refund_error', esc_html__( 'Order already refunded', 'fast-events' ), array( 'status' => 404 ) );
		}

		// The order is paid, but there is no payment_id (manually changed) or the order is created from the dashboard.
		if ( ( 'paid' === $order->payment_status && '' === $order->payment_id ) || 'dashboard' === $order->payment_status ) {
			return new WP_Error( 'refund_error', esc_html__( 'Order was manually added and has no payment_id', 'fast-events' ), array( 'status' => 404 ) );
		}

		// Was the order paid?
		if ( 'paid' !== $order->payment_status ) {
			return new WP_Error( 'refund_error', esc_html__( 'Order is not paid', 'fast-events' ), array( 'status' => 404 ) );
		}

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

		// Get the refund costs.
		$refund_cost = $this->settings->refund_costs();

		// Calculate refund amount.
		if ( 1 === $this->settings->refund_per() ) {
			$refund_amount = (float) $order->total - $refund_cost;
		} else {
			$refund_amount = (float) $order->total - ( (float) $order->num_tickets * $refund_cost );
		}

		// Do the refund.
		$refund = $fevt_container->get( PaymentFactory::class )->refund(
			$order,
			esc_html__( 'Refunded order #', 'fast-events' ) . $order_id,
			$refund_amount,
			array(
				'order_id'     => $order_id,
				'event_id'     => (int) $order->event_id,
				'fe_order_uid' => $order->uid,
				'fe_version'   => $GLOBALS['fast_events']::FAST_EVENTS_VERSION,
			),
			(int) $event->saas_user_id,
			(bool) $event->test_payments_needed
		);
		if ( is_wp_error( $refund ) ) {
			return $refund;
		}

		// Set the new payment status.
		$resp = $wpdb->query(
			"UPDATE {$wpdb->prefix}fe_event_orders SET payment_status = 'refunded', " .
			"input_fields = JSON_INSERT(input_fields, '$.metadata', JSON_OBJECT(), '$.metadata.has_refunds', '1' ) " . // phpcs:ignore
			"WHERE id = $order_id" // phpcs:ignore
		);
		if ( false !== $resp && 0 !== $resp ) {

			// Delete all tickets and update counters.
			$this->events->fast_events_delete_tickets( array( (int) $order->event_id ), array( $order_id ) );

			// Fire webhooks if needed.
			if ( 1 === (int) $event->webhooks_needed ) {
				WebhookUtil::create_webhook( 'order.refunded', $order_id );
			}
		}

		return esc_html__( 'Refunded order #', 'fast-events' ) . $order_id;
	}


	/**
	 * Create a new dashboard order
	 *
	 * @param WP_REST_Request $data Request instance.
	 * @return stdClass|WP_Error
	 * @throws Exception Form error.
	 *
	 * @since 1.0
	 * @since 1.4.0 - Event authorization restriction
	 *              - Added sender_name and sender_email to query
	 * @since 2.2.0 Added 'coupons_needed' to the query
	 * @since 2.3.0 Use fast_events_json_encode.
	 */
	public function new_dashboard_order( WP_REST_Request $data ): WP_Error|stdClass {
		global $wpdb;

		$params     = $data->get_params();
		$event_info = json_decode( $params['event_info'] );
		$first_id   = array_key_first( (array) $event_info );

		// Check the event authorization.
		$resp = $this->auth->check_event_authorization( absint( $first_id ) );
		if ( is_wp_error( $resp ) ) {
			return $resp;
		}

		// Check if the event exists.
		$event_table = $wpdb->prefix . 'fe_events';
		$event       = $wpdb->get_row(
			$wpdb->prepare(
				"SELECT add_dashboard_orders_needed, emails_needed, tickets_needed, seats_needed, coupons_needed FROM {$wpdb->prefix}fe_events WHERE id = %d",
				$first_id
			)
		);
		if ( empty( $event ) ) {
			return new WP_Error( 'not_found', esc_html__( 'Event not found', 'fast-events' ), array( 'status' => 404 ) );
		}

		// Are we allowed to add orders?
		if ( ! $event->add_dashboard_orders_needed ) {
			return new WP_Error( 'not_allowed', esc_html__( 'You are not allowed to add orders', 'fast-events' ), array( 'status' => 400 ) );
		}

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

		// Check the form input.
		$error = $this->events->validate_form_input( $data, 'dashboard' );
		if ( is_wp_error( $error ) ) {
			$wpdb->query( 'ROLLBACK' );
			$this->events->process_delayed_error();
			return $error;
		}

		// Update the order with the updated input_fields.
		$order_table = $wpdb->prefix . 'fe_event_orders';
		foreach ( $this->events->get_order_ids() as $order_id ) {
			$wpdb->query(
				$wpdb->prepare(
					"UPDATE {$wpdb->prefix}fe_event_orders SET input_fields=%s, payment_status=%s WHERE id=%d",
					JsonUtil::fast_events_json_encode( $this->events->get_json_input_fields( $order_id ) ),
					'dashboard',
					$order_id
				)
			);
		}

		// Everything went well -> commit the transaction.
		$wpdb->query( 'COMMIT' );

		// Generate tickets and emails.
		foreach ( $this->events->get_order_ids() as $order_id ) {
			// phpcs:disable
			$result = $wpdb->get_row(
				$wpdb->prepare(
					"SELECT $order_table.id, $order_table.uid, $order_table.payment_status, $order_table.event_id, " .
					"$order_table.input_fields, $order_table.name, $order_table.email, $order_table.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.test_payments_needed, $event_table.seats_needed, $event_table.webhooks_needed, " .
					"$event_table.emails_needed, $event_table.confirmation_emails_needed, $event_table.confirmation_timeout, $event_table.redirect, " .
					"$event_table.tracking_needed, $event_table.group_type, $event_table.event_group  " .
					"FROM $order_table " .
					"INNER JOIN $event_table ON $event_table.id = $order_table.event_id " .
					"WHERE $order_table.id = %d",
					$order_id
				)
			);
			// phpcs:enable

			// Does the key exist?
			if ( null === $result ) {
				return new WP_Error( 'not_found', esc_html__( 'Order not found', 'fast-events' ), array( 'status' => 404 ) );
			}

			// Create tickets and send email.
			$this->events->fast_events_create_tickets_and_send_email( $this->events->get_order_ids(), $this->events->get_event_ids(), $result, MailerInterface::FAST_EVENTS_NORMAL_EMAIL, $result->payment_status );
		}

		// As this is a dashboard order, it is always a single order.
		// Set the total_amount and discount amount to the right value.
		// With dashboard orders, there are no coupons.
		// For VAT, we assume that it is the same for all tickets. We select the first one.
		$order_id        = $this->events->get_order_ids()[0];
		$order_amount    = (float) ( $params['amount'] ?? '0.00' );
		$discount_amount = $this->events->get_amount() - $order_amount;
		$input_fields    = $this->events->get_json_input_fields( $order_id );
		$vat             = (float) $input_fields->tickets[0]['vat'];
		$order_vat       = $order_amount - ( ( $order_amount / ( $vat + 100 ) ) * 100 );
		$order_format    = number_format( $order_amount, 4, '.', '' );
		$vat_format      = number_format( $order_vat, 4, '.', '' );
		$wpdb->query(
			$wpdb->prepare(
				"UPDATE {$wpdb->prefix}fe_event_orders SET " .
				"total = %f, discount = %f, input_fields = JSON_SET(input_fields, '$.order_amount', %s, '$.order_vat', %s," .
				"'$.discounts', JSON_ARRAY(JSON_OBJECT('code', '__dashboard-order__', 'amount', %f))) " .
				'WHERE id = %d',
				$order_amount,
				$discount_amount,
				$order_format,
				$vat_format,
				$discount_amount,
				$order_id
			)
		);

		// Create the response structure.
		$resp              = new stdClass();
		$resp->id          = $this->events->get_order_ids()[0];
		$resp->event_date  = $this->events->get_event_date();
		$resp->created_at  = current_time( 'mysql' );
		$resp->name        = sanitize_text_field( $params['name'] );
		$resp->email       = sanitize_text_field( $params['email'] );
		$resp->num_tickets = $this->events->get_num_tickets();
		$resp->total       = $order_format;

		/* translators: %d is the order id */
		$resp->message = sprintf( esc_html__( 'Order #%d added', 'fast-events' ), $this->events->get_order_ids()[0] );

		return $resp;
	}
}
