<?php
/**
* Handles the AJAX requests for the Seating feature.
*
* @since 5.16.0
*
* @package TEC\Tickets\Seating\Admin;
*/
namespace TEC\Tickets\Seating\Admin;
use TEC\Common\Contracts\Container;
use TEC\Common\Contracts\Provider\Controller as Controller_Contract;
use TEC\Common\Asset;
use TEC\Common\StellarWP\DB\DB;
use TEC\Tickets\Commerce\Cart;
use TEC\Tickets\Commerce\Module;
use TEC\Tickets\Seating\Admin;
use TEC\Tickets\Seating\Admin\Tabs\Layout_Edit;
use TEC\Tickets\Seating\Ajax_Methods;
use TEC\Tickets\Seating\Commerce\Controller;
use TEC\Tickets\Seating\Logging;
use TEC\Tickets\Seating\Meta;
use TEC\Tickets\Seating\Service\Layouts;
use TEC\Tickets\Seating\Service\Maps;
use TEC\Tickets\Seating\Service\Reservations;
use TEC\Tickets\Seating\Service\Seat_Types;
use TEC\Tickets\Seating\Tables\Sessions;
use Tribe__Tickets__Tickets as Tickets;
use Tribe__Tickets__Main as Tickets_Main;
use Tribe__Tickets__Global_Stock as Global_Stock;
/**
* Class Ajax.
*
* @since 5.16.0
*
* @package TEC\Tickets\Seating\Admin;
*/
class Ajax extends Controller_Contract {
use Ajax_Methods;
use Logging;
/**
* The nonce action.
*
* @since 5.16.0
*
* @var string
*/
public const NONCE_ACTION = 'tec-tickets-seating-service-ajax';
/**
* The action to get the seat types for a given layout ID.
*
* @since 5.16.0
*
* @var string
*/
public const ACTION_GET_SEAT_TYPES_BY_LAYOUT_ID = 'tec_tickets_seating_get_seat_types_by_layout_id';
/**
* The action to invalidate the maps and layouts cache.
*
* @since 5.16.0
*
* @var string
*/
public const ACTION_INVALIDATE_MAPS_LAYOUTS_CACHE = 'tec_tickets_seating_service_invalidate_maps_layouts_cache';
/**
* The action to invalidate the layouts cache.
*
* @since 5.16.0
*
* @var string
*/
public const ACTION_INVALIDATE_LAYOUTS_CACHE = 'tec_tickets_seating_service_invalidate_layouts_cache';
/**
* The action to delete a map.
*
* @since 5.16.0
*
* @var string
*/
public const ACTION_DELETE_MAP = 'tec_tickets_seating_service_delete_map';
/**
* The action to delete a layout.
*
* @since 5.16.0
*
* @var string
*/
public const ACTION_DELETE_LAYOUT = 'tec_tickets_seating_service_delete_layout';
/**
* The action to duplicate a layout.
*
* @since 5.17.0
*
* @var string
*/
public const ACTION_DUPLICATE_LAYOUT = 'tec_tickets_seating_service_duplicate_layout';
/**
* The action to add a layout.
*
* @since 5.16.0
*
* @var string
*/
public const ACTION_ADD_NEW_LAYOUT = 'tec_tickets_seating_service_add_layout';
/**
* The action to push the reservations to the backend from the seat-selection frontend.
*
* @since 5.16.0
*
* @var string
*/
public const ACTION_POST_RESERVATIONS = 'tec_tickets_seating_post_reservations';
/**
* The action to remove the reservations from the backend from the seat-selection frontend.
*
* @since 5.16.0
*
* @var string
*/
public const ACTION_CLEAR_RESERVATIONS = 'tec_tickets_seating_clear_reservations';
/**
* The action to delete reservations.
*
* @since 5.16.0
*
* @var string
*/
public const ACTION_DELETE_RESERVATIONS = 'tec_tickets_seating_delete_reservations';
/**
* The action to fetch attendees.
*
* @since 5.16.0
*
* @var string
*/
public const ACTION_FETCH_ATTENDEES = 'tec_tickets_seating_fetch_attendees';
/**
* The action to update a set of seat types.
*
* @since 5.16.0
*
* @var string
*/
public const ACTION_SEAT_TYPES_UPDATED = 'tec_tickets_seating_seat_types_updated';
/**
* The action to handle seat type deletion.
*
* @since 5.16.0
*
* @var string
*/
public const ACTION_SEAT_TYPE_DELETED = 'tec_tickets_seating_seat_type_deleted';
/**
* The action to update a set of reservations following a seat type update.
*
* @since 5.16.0
*
* @var string
*/
public const ACTION_RESERVATIONS_UPDATED_FROM_SEAT_TYPES = 'tec_tickets_seating_reservations_updated_from_seat_types';
/**
* The action to create a new reservation from the Seats Report page.
*
* @since 5.16.0
*
* @var string
*/
public const ACTION_RESERVATION_CREATED = 'tec_tickets_seating_reservation_created';
/**
* The action to update an existing reservation from the Seats Report page.
*
* @since 5.16.0
*
* @var string
*/
public const ACTION_RESERVATION_UPDATED = 'tec_tickets_seating_reservation_updated';
/**
* The action to update the layout for an event.
*
* @since 5.16.0
*
* @var string
*/
public const ACTION_EVENT_LAYOUT_UPDATED = 'tec_tickets_seating_event_layout_updated';
/**
* The action to remove the post layout.
*
* @since 5.18.0
*
* @var string
*/
public const ACTION_EVENT_LAYOUT_REMOVE = 'tec_tickets_seating_event_layout_removal';
/**
* A reference to the Seat Types service object.
*
* @since 5.16.0
*
* @var Seat_Types
*/
private Seat_Types $seat_types;
/**
* A reference to the Sessions table object.
*
* @since 5.16.0
*
* @var Sessions
*/
private Sessions $sessions;
/**
* A reference to the Reservations service object.
*
* @since 5.16.0
*
* @var Reservations
*/
private Reservations $reservations;
/**
* A reference to the Maps service object.
*
* @since 5.16.0
*
* @var Maps
*/
private Maps $maps;
/**
* A reference to the Layouts service object.
*
* @since 5.16.0
*
* @var Layouts
*/
private Layouts $layouts;
/**
* Ajax constructor.
*
* @since 5.16.0
*
* @param Container $container A reference to the DI container object.
* @param Seat_Types $seat_types A reference to the Seat Types service object.
* @param Sessions $sessions A reference to the Sessions table object.
* @param Reservations $reservations A reference to the Reservations service object.
* @param Maps $maps A reference to the Maps service object.
* @param Layouts $layouts A reference to the Layouts service object.
*/
public function __construct(
Container $container,
Seat_Types $seat_types,
Sessions $sessions,
Reservations $reservations,
Maps $maps,
Layouts $layouts
) {
parent::__construct( $container );
$this->seat_types = $seat_types;
$this->sessions = $sessions;
$this->reservations = $reservations;
$this->maps = $maps;
$this->layouts = $layouts;
}
/**
* Registers the controller bindings and subscribes to WordPress hooks.
*
* @since 5.16.0
*/
protected function do_register(): void {
$this->register_assets();
add_action( 'wp_ajax_' . self::ACTION_GET_SEAT_TYPES_BY_LAYOUT_ID, [ $this, 'fetch_seat_types_by_layout_id' ] );
add_action(
'wp_ajax_' . self::ACTION_INVALIDATE_MAPS_LAYOUTS_CACHE,
[ $this, 'invalidate_maps_layouts_cache' ]
);
add_action( 'wp_ajax_' . self::ACTION_INVALIDATE_LAYOUTS_CACHE, [ $this, 'invalidate_layouts_cache' ] );
add_action( 'wp_ajax_' . self::ACTION_DELETE_MAP, [ $this, 'delete_map_from_service' ] );
add_action( 'wp_ajax_' . self::ACTION_DELETE_LAYOUT, [ $this, 'delete_layout_from_service' ] );
add_action( 'wp_ajax_' . self::ACTION_ADD_NEW_LAYOUT, [ $this, 'add_new_layout_to_service' ] );
add_action( 'wp_ajax_' . self::ACTION_DUPLICATE_LAYOUT, [ $this, 'duplicate_layout_in_service' ] );
add_action( 'wp_ajax_' . self::ACTION_POST_RESERVATIONS, [ $this, 'update_reservations' ] );
add_action( 'wp_ajax_nopriv_' . self::ACTION_POST_RESERVATIONS, [ $this, 'update_reservations' ] );
add_action( 'wp_ajax_' . self::ACTION_CLEAR_RESERVATIONS, [ $this, 'clear_reservations' ] );
add_action( 'wp_ajax_nopriv_' . self::ACTION_CLEAR_RESERVATIONS, [ $this, 'clear_reservations' ] );
add_action( 'wp_ajax_' . self::ACTION_DELETE_RESERVATIONS, [ $this, 'delete_reservations' ] );
add_action( 'wp_ajax_' . self::ACTION_SEAT_TYPES_UPDATED, [ $this, 'update_seat_types' ] );
add_action(
'wp_ajax_' . self::ACTION_RESERVATIONS_UPDATED_FROM_SEAT_TYPES,
[ $this, 'update_reservations_from_seat_types' ]
);
add_action( 'wp_ajax_' . self::ACTION_SEAT_TYPE_DELETED, [ $this, 'handle_seat_type_deleted' ] );
add_action( 'wp_ajax_' . self::ACTION_EVENT_LAYOUT_UPDATED, [ $this, 'update_event_layout' ] );
add_action( 'wp_ajax_' . self::ACTION_EVENT_LAYOUT_REMOVE, [ $this, 'remove_event_layout' ] );
add_action( 'tec_tickets_seating_session_interrupt', [ $this, 'clear_commerce_cart_cookie' ] );
}
/**
* Unsubscribes the controller from WordPress hooks.
*
* @since 5.16.0
*
* @return void
*/
public function unregister(): void {
remove_action( 'wp_ajax_' . self::ACTION_GET_SEAT_TYPES_BY_LAYOUT_ID, [ $this, 'fetch_seat_types_by_layout_id' ] );
remove_action(
'wp_ajax_' . self::ACTION_INVALIDATE_MAPS_LAYOUTS_CACHE,
[ $this, 'invalidate_maps_layouts_cache' ]
);
remove_action( 'wp_ajax_' . self::ACTION_INVALIDATE_LAYOUTS_CACHE, [ $this, 'invalidate_layouts_cache' ] );
remove_action( 'wp_ajax_' . self::ACTION_DELETE_MAP, [ $this, 'delete_map_from_service' ] );
remove_action( 'wp_ajax_' . self::ACTION_DELETE_LAYOUT, [ $this, 'delete_layout_from_service' ] );
remove_action( 'wp_ajax_' . self::ACTION_ADD_NEW_LAYOUT, [ $this, 'add_new_layout_to_service' ] );
remove_action( 'wp_ajax_' . self::ACTION_DUPLICATE_LAYOUT, [ $this, 'duplicate_layout_in_service' ] );
remove_action( 'wp_ajax_' . self::ACTION_POST_RESERVATIONS, [ $this, 'update_reservations' ] );
remove_action( 'wp_ajax_nopriv_' . self::ACTION_POST_RESERVATIONS, [ $this, 'update_reservations' ] );
remove_action( 'wp_ajax_' . self::ACTION_CLEAR_RESERVATIONS, [ $this, 'clear_reservations' ] );
remove_action( 'wp_ajax_nopriv_' . self::ACTION_CLEAR_RESERVATIONS, [ $this, 'clear_reservations' ] );
remove_action( 'tec_tickets_seating_session_interrupt', [ $this, 'clear_commerce_cart_cookie' ] );
remove_action( 'wp_ajax_' . self::ACTION_DELETE_RESERVATIONS, [ $this, 'delete_reservations' ] );
remove_action( 'wp_ajax_' . self::ACTION_SEAT_TYPES_UPDATED, [ $this, 'update_seat_types' ] );
remove_action(
'wp_ajax_' . self::ACTION_RESERVATIONS_UPDATED_FROM_SEAT_TYPES,
[ $this, 'update_reservations_from_seat_types' ]
);
remove_action( 'wp_ajax_' . self::ACTION_SEAT_TYPE_DELETED, [ $this, 'handle_seat_type_deleted' ] );
remove_action( 'wp_ajax_' . self::ACTION_EVENT_LAYOUT_UPDATED, [ $this, 'update_event_layout' ] );
remove_action( 'wp_ajax_' . self::ACTION_EVENT_LAYOUT_REMOVE, [ $this, 'remove_event_layout' ] );
}
/**
* Returns the Ajax data for the Seating feature.
*
* @since 5.16.0
*
* @return array<string,string> The Ajax data for the Seating feature.
*/
public function get_ajax_data(): array {
return [
'ajaxUrl' => admin_url( 'admin-ajax.php' ),
'ajaxNonce' => wp_create_nonce( self::NONCE_ACTION ),
'ACTION_INVALIDATE_MAPS_LAYOUTS_CACHE' => self::ACTION_INVALIDATE_MAPS_LAYOUTS_CACHE,
'ACTION_INVALIDATE_LAYOUTS_CACHE' => self::ACTION_INVALIDATE_LAYOUTS_CACHE,
'ACTION_DELETE_MAP' => self::ACTION_DELETE_MAP,
'ACTION_DELETE_LAYOUT' => self::ACTION_DELETE_LAYOUT,
'ACTION_ADD_NEW_LAYOUT' => self::ACTION_ADD_NEW_LAYOUT,
'ACTION_DUPLICATE_LAYOUT' => self::ACTION_DUPLICATE_LAYOUT,
'ACTION_POST_RESERVATIONS' => self::ACTION_POST_RESERVATIONS,
'ACTION_CLEAR_RESERVATIONS' => self::ACTION_CLEAR_RESERVATIONS,
'ACTION_DELETE_RESERVATIONS' => self::ACTION_DELETE_RESERVATIONS,
'ACTION_FETCH_ATTENDEES' => self::ACTION_FETCH_ATTENDEES,
'ACTION_GET_SEAT_TYPES_BY_LAYOUT_ID' => self::ACTION_GET_SEAT_TYPES_BY_LAYOUT_ID,
'ACTION_SEAT_TYPES_UPDATED' => self::ACTION_SEAT_TYPES_UPDATED,
'ACTION_SEAT_TYPE_DELETED' => self::ACTION_SEAT_TYPE_DELETED,
'ACTION_RESERVATIONS_UPDATED_FROM_SEAT_TYPES' => self::ACTION_RESERVATIONS_UPDATED_FROM_SEAT_TYPES,
'ACTION_RESERVATION_CREATED' => self::ACTION_RESERVATION_CREATED,
'ACTION_RESERVATION_UPDATED' => self::ACTION_RESERVATION_UPDATED,
'ACTION_EVENT_LAYOUT_UPDATED' => self::ACTION_EVENT_LAYOUT_UPDATED,
'ACTION_REMOVE_EVENT_LAYOUT' => self::ACTION_EVENT_LAYOUT_REMOVE,
];
}
/**
* Registers the assets used by the AJAX component.
*
* @since 5.16.0
*/
private function register_assets(): void {
Asset::add(
'tec-tickets-seating-ajax',
'ajax.js',
Tickets_Main::VERSION
)
->add_to_group_path( 'tec-seating' )
->add_localize_script( 'tec.tickets.seating.ajaxData', [ $this, 'get_ajax_data' ] )
->add_to_group( 'tec-tickets-seating' )
->register();
}
/**
* Returns the seat types in option format for the given layout IDs.
*
* @since 5.16.0
*
* @return void The seat types in option format for the given layout IDs are returned as JSON.
*/
public function fetch_seat_types_by_layout_id(): void {
if ( ! $this->check_current_ajax_user_can( 'edit_posts' ) ) {
return;
}
$layout_id = tribe_get_request_var( 'layout' );
if ( empty( $layout_id ) ) {
wp_send_json_success( [] );
return;
}
$seat_types = $this->seat_types->get_in_option_format( [ $layout_id ] );
wp_send_json_success( $seat_types );
}
/**
* Invalidates the Maps and Layouts caches.
*
* @since 5.16.0
*
* @return void The function does not return a value but will echo the JSON response.
*/
public function invalidate_maps_layouts_cache(): void {
if ( ! $this->check_current_ajax_user_can( 'manage_options' ) ) {
return;
}
if ( ! Layouts::invalidate_cache() ) {
wp_send_json_error( [ 'error' => 'Failed to invalidate the layouts cache.' ], 500 );
return;
}
if ( ! ( Maps::invalidate_cache() ) ) {
wp_send_json_error( [ 'error' => 'Failed to invalidate the maps layouts cache.' ], 500 );
return;
}
wp_send_json_success();
}
/**
* Invalidates the Layouts cache.
*
* @since 5.16.0
*
* @return void The function does not return a value but will echo the JSON response.
*/
public function invalidate_layouts_cache(): void {
if ( ! $this->check_current_ajax_user_can( 'manage_options' ) ) {
return;
}
if ( ! ( Layouts::invalidate_cache() ) ) {
wp_send_json_error( [ 'error' => 'Failed to invalidate the layouts cache.' ], 500 );
}
wp_send_json_success();
}
/**
* Deletes a map from the service.
*
* @since 5.16.0
*
* @return void The function does not return a value but will send the JSON response.
*/
public function delete_map_from_service(): void {
if ( ! $this->check_current_ajax_user_can( 'manage_options' ) ) {
return;
}
$map_id = (string) tribe_get_request_var( 'mapId' );
if ( empty( $map_id ) ) {
wp_send_json_error(
[
'error' => __( 'No map ID provided', 'event-tickets' ),
],
400
);
return;
}
if ( $this->maps->delete( $map_id ) ) {
wp_send_json_success();
return;
}
wp_send_json_error( [ 'error' => __( 'Failed to delete the map.', 'event-tickets' ) ], 500 );
}
/**
* Deletes a layout from the service.
*
* @since 5.16.0
*
* @return void The function does not return a value but will send the JSON response.
*/
public function delete_layout_from_service(): void {
if ( ! $this->check_current_ajax_user_can( 'manage_options' ) ) {
return;
}
$layout_id = (string) tribe_get_request_var( 'layoutId' );
$map_id = (string) tribe_get_request_var( 'mapId' );
if ( empty( $layout_id ) || empty( $map_id ) ) {
wp_send_json_error(
[
'error' => __( 'No layout ID or map ID provided', 'event-tickets' ),
],
400
);
return;
}
if ( $this->layouts->delete( $layout_id, $map_id ) ) {
wp_send_json_success();
return;
}
wp_send_json_error( [ 'error' => __( 'Failed to delete the layout.', 'event-tickets' ) ], 500 );
}
/**
* Adds a new layout to the service.
*
* @since 5.16.0
*
* @return void The function does not return a value but will send the JSON response.
*/
public function add_new_layout_to_service(): void {
if ( ! $this->check_current_ajax_user_can( 'manage_options' ) ) {
return;
}
$map_id = (string) tribe_get_request_var( 'mapId' );
if ( empty( $map_id ) ) {
wp_send_json_error(
[
'error' => __( 'No map ID provided', 'event-tickets' ),
],
400
);
return;
}
$layout_id = $this->layouts->add( $map_id );
if ( ! empty( $layout_id ) ) {
$edit_url = add_query_arg(
[
'page' => Admin::get_menu_slug(),
'tab' => Layout_Edit::get_id(),
'layoutId' => $layout_id,
'isNew' => 1,
],
admin_url( 'admin.php' )
);
wp_send_json_success( $edit_url );
return;
}
wp_send_json_error( [ 'error' => __( 'Failed to Add new layout.', 'event-tickets' ) ], 500 );
}
/**
* Duplicates a layout in the service.
*
* @since 5.17.0
*
* @return void The function does not return a value but will send the JSON response.
*/
public function duplicate_layout_in_service(): void {
if ( ! $this->check_current_ajax_user_can( 'manage_options' ) ) {
return;
}
$layout_id = (string) tribe_get_request_var( 'layoutId' );
if ( empty( $layout_id ) ) {
wp_send_json_error(
[
'error' => __( 'No layout ID provided for duplication', 'event-tickets' ),
],
400
);
return;
}
$duplicated_layout_id = $this->layouts->duplicate_layout( $layout_id );
if ( empty( $duplicated_layout_id ) ) {
wp_send_json_error( [ 'error' => __( 'Failed to duplicate layout.', 'event-tickets' ) ], 500 );
return;
}
$edit_url = add_query_arg(
[
'page' => Admin::get_menu_slug(),
'tab' => Layout_Edit::get_id(),
'layoutId' => $duplicated_layout_id,
'isNew' => 1,
],
admin_url( 'admin.php' )
);
wp_send_json_success( $edit_url );
}
/**
* Handles the request to update reservations on the Service.
*
* @since 5.16.0
*
* @return void The JSON response is sent to the client.
*/
public function update_reservations() {
if ( ! $this->check_current_ajax_user_can( 'exist' ) ) {
return;
}
$post_id = (int) tribe_get_request_var( 'postId', 0 );
if ( empty( $post_id ) ) {
wp_send_json_error(
[
'error' => 'No post ID provided',
],
400
);
return;
}
$body = $this->get_request_body();
$decoded = json_decode( $body, true );
if ( ! (
$decoded
&& is_array( $decoded )
&& isset( $decoded['token'], $decoded['reservations'] )
&& is_string( $decoded['token'] )
&& is_array( $decoded['reservations'] )
) ) {
wp_send_json_error(
[
'error' => 'Invalid request body',
],
400
);
return;
}
$token = $decoded['token'];
$json_reservations = $decoded['reservations'];
$reservations = [];
foreach ( $json_reservations as $ticket_id => $ticket_reservations ) {
$reservations[ $ticket_id ] = [];
if ( ! is_array( $ticket_reservations ) ) {
wp_send_json_error(
[
'error' => 'Reservation data is not in correct format',
],
400
);
return;
}
foreach ( $ticket_reservations as $reservation ) {
if ( ! (
is_array( $reservation )
&& isset( $reservation['reservationId'], $reservation['seatTypeId'], $reservation['seatLabel'] )
) ) {
wp_send_json_error(
[
'error' => 'Reservation data is not in correct format',
],
400
);
return;
}
$reservations[ $ticket_id ][] = [
'reservation_id' => $reservation['reservationId'],
'seat_type_id' => $reservation['seatTypeId'],
'seat_label' => $reservation['seatLabel'],
];
}
}
if ( ! ( $this->sessions->update_reservations( $token, $reservations ) ) ) {
wp_send_json_error(
[
'error' => 'Failed to update the reservations',
],
500
);
return;
}
wp_send_json_success();
}
/**
* Handles the request to remove reservations on the Service.
*
* @since 5.16.0
*
* @return void The JSON response is sent to the client.
*/
public function clear_reservations(): void {
if ( ! $this->check_current_ajax_user_can( 'exist' ) ) {
return;
}
$post_id = (int) tribe_get_request_var( 'postId', 0 );
$token = tribe_get_request_var( 'token' );
if ( ! ( $post_id && $token && is_string( $token ) ) ) {
wp_send_json_error(
[
'error' => __( 'Invalid request parameters', 'event-tickets' ),
],
400
);
return;
}
if ( ! (
$this->reservations->cancel( $post_id, $this->sessions->get_reservation_uuids_for_token( $token ) )
&& $this->sessions->delete_token_session( $token )
) ) {
wp_send_json_error(
[
'error' => __( 'Failed to clear the reservations', 'event-tickets' ),
],
500
);
return;
}
wp_send_json_success();
}
/**
* Removes the Tribe Commerce cart cookie when a seat selection session is interrupted.
*
* @since 5.16.0
*
* @param int $post_id The post ID the session is being interrupted for.
*
* @return void The cookie is cleared.
*/
public function clear_commerce_cart_cookie( int $post_id ): void {
if ( Tickets::get_event_ticket_provider( $post_id ) !== Module::class ) {
return;
}
// Remove the `tec-tickets-commerce-cart` cookie.
$cookie_name = Cart::$cart_hash_cookie_name;
// phpcs:ignore WordPressVIPMinimum.Functions.RestrictedFunctions.cookies_setcookie
setcookie(
$cookie_name,
'',
time() - DAY_IN_SECONDS,
COOKIEPATH,
COOKIE_DOMAIN,
true,
true
);
// phpcs:ignore WordPressVIPMinimum.Variables.RestrictedVariables.cache_constraints___COOKIE
unset( $_COOKIE[ $cookie_name ] );
}
/**
* Handles the request to delete reservations from attendees.
*
* @since 5.16.0
*
* @return void The function does not return a value but will send the JSON response.
*/
public function delete_reservations(): void {
if ( ! $this->check_current_ajax_user_can( 'manage_options' ) ) {
wp_send_json_error(
[
'error' => 'Nonce verification failed',
],
403
);
return;
}
$body = $this->get_request_body();
$json = json_decode( $body, true );
if ( ! ( is_array( $json ) ) ) {
wp_send_json_error(
[
'error' => 'Invalid request body',
],
400
);
return;
}
$reservation_ids = array_map( 'strval', array_filter( $json ) );
if ( empty( $reservation_ids ) ) {
wp_send_json_error(
[
'error' => 'Invalid request body',
],
400
);
return;
}
try {
$deleted = $this->reservations->delete_reservations_from_attendees( $reservation_ids );
} catch ( \Exception $e ) {
$this->log_error(
'Failed to delete reservations from attendees.',
[
'source' => __METHOD__,
'error' => $e->getMessage(),
]
);
wp_send_json_error(
[
'error' => 'Failed to delete reservations from attendees',
],
500
);
}
wp_send_json_success( [ 'numberDeleted' => $deleted ] );
}
/**
* Handles the update of seat types from the service.
*
* @since 5.16.0
*
* @return void The function does not return a value but will echo the JSON response.
*/
public function update_seat_types(): void {
if ( ! $this->check_current_ajax_user_can( 'manage_options' ) ) {
wp_send_json_error(
[
'error' => 'Nonce verification failed',
],
403
);
return;
}
$body = $this->get_request_body();
$decoded = json_decode( $body, true );
if ( ! ( $decoded && is_array( $decoded ) ) ) {
wp_send_json_error(
[
'error' => 'Invalid request body',
],
400
);
return;
}
$valid = array_filter(
$decoded,
static function ( $updated_seat_type ) {
return is_array( $updated_seat_type )
&& isset(
$updated_seat_type['id'],
$updated_seat_type['name'],
$updated_seat_type['mapId'],
$updated_seat_type['layoutId'],
$updated_seat_type['description'],
$updated_seat_type['seatsCount'],
);
}
);
if ( empty( $valid ) ) {
wp_send_json_error(
[
'error' => 'Invalid request body',
],
400
);
return;
}
$updated_seat_types = $this->seat_types->update_from_service( $valid );
if ( false === $updated_seat_types ) {
wp_send_json_error(
[
'error' => 'Failed to update the seat types from the service.',
],
500
);
return;
}
$seat_type_to_capacity_map = array_reduce(
$valid,
static function ( array $carry, array $seat_type ): array {
return $carry + [ $seat_type['id'] => $seat_type['seatsCount'] ];
},
[]
);
$updated_tickets = $this->seat_types->update_tickets_capacity( $seat_type_to_capacity_map );
$layout_to_seats_map = array_reduce(
$valid,
static function ( array $carry, array $seat_type ): array {
$layout_id = $seat_type['layoutId'];
if ( isset( $carry[ $layout_id ] ) ) {
$carry[ $layout_id ] += $seat_type['seatsCount'];
} else {
$carry[ $layout_id ] = $seat_type['seatsCount'];
}
return $carry;
},
[]
);
$updated_posts = $this->layouts->update_posts_capacity( $layout_to_seats_map );
wp_send_json_success(
[
'updatedSeatTypes' => $updated_seat_types,
'updatedTickets' => $updated_tickets,
'updatedPosts' => $updated_posts,
]
);
}
/**
* Updates the seat type of Attendees following based on their reservation.
*
* @since 5.16.0
*
* @return void The function does not return a value but will send the JSON response.
*/
public function update_reservations_from_seat_types(): void {
if ( ! $this->check_current_ajax_user_can( 'manage_options' ) ) {
wp_send_json_error(
[
'error' => 'Nonce verification failed',
],
403
);
return;
}
$body = $this->get_request_body();
$decoded = json_decode( $body, true );
if ( ! ( $decoded && is_array( $decoded ) ) ) {
wp_send_json_error(
[
'error' => 'Invalid request body',
],
400
);
return;
}
$valid = array_filter( $decoded, 'is_array' );
if ( empty( $valid ) ) {
wp_send_json_error(
[
'error' => 'Invalid request body',
],
400
);
return;
}
$updated = $this->reservations->update_attendees_seat_type( $valid );
wp_send_json_success( [ 'updatedAttendees' => $updated ] );
}
/**
* Handles the deletion of a seat type by transferring existing reservations to new seat type.
*
* @since 5.16.0
*
* @return void The function does not return a value but will send the JSON response.
*/
public function handle_seat_type_deleted() {
if ( ! $this->check_current_ajax_user_can( 'manage_options' ) ) {
wp_send_json_error(
[
'error' => 'Nonce verification failed',
],
403
);
return;
}
$decoded = $this->get_request_json();
$old_seat_type_id = $decoded['deletedId'] ?? null;
$new_seat_type = $decoded['transferTo'] ?? null;
if ( empty( $old_seat_type_id )
|| ! is_array( $new_seat_type )
|| ! isset(
$new_seat_type['id'],
$new_seat_type['name'],
$new_seat_type['mapId'],
$new_seat_type['layoutId'],
$new_seat_type['description'],
$new_seat_type['seatsCount']
) ) {
wp_send_json_error(
[
'error' => 'Invalid request body',
],
400
);
return;
}
/*
* Update all Tickets and Attendees with old seat type meta to new seat type.
* Note this updated is NOT done based on currently active Tickets and Attendee types
* to include, in the update, those that are not currently active.
*/
global $wpdb;
$ticket_post_types = implode( ', ', array_map( static fn( $v ) => "'" . esc_sql( $v ) . "'", array_values( tribe_tickets()->ticket_types() ) ) );
try {
$original_seat_types_tickets = array_map(
'intval',
DB::get_col(
DB::prepare(
'SELECT DISTINCT(pm.post_id) FROM %i pm JOIN %i p ON p.ID=pm.post_id WHERE pm.meta_key = %s AND pm.meta_value = %s AND p.post_type IN (' . $ticket_post_types . ')',
$wpdb->postmeta,
$wpdb->posts,
Meta::META_KEY_SEAT_TYPE,
$new_seat_type['id']
)
)
);
$updated_seat_types_meta = DB::query(
DB::prepare(
'UPDATE %i SET meta_value = %s WHERE meta_key = %s AND meta_value = %s',
$wpdb->postmeta,
$new_seat_type['id'],
Meta::META_KEY_SEAT_TYPE,
$old_seat_type_id,
),
);
} catch ( \Exception $exception ) {
$this->log_error(
'Failed to update seat type meta.',
[
'source' => __METHOD__,
'error' => $exception->getMessage(),
]
);
}
$updated_seat_types = $this->seat_types->update_from_service( [ $new_seat_type ] );
$updated_tickets = $this->seat_types->update_tickets_with_calculated_stock_and_capacity( $new_seat_type['id'], $new_seat_type['seatsCount'], $original_seat_types_tickets );
wp_send_json_success(
[
'updatedSeatTypes' => $updated_seat_types,
'updatedTickets' => $updated_tickets,
'updatedMeta' => $updated_seat_types_meta,
]
);
}
/**
* Updates the layout of an event.
*
* @since 5.16.0
*
* @return void The function does not return a value but will echo the JSON response.
*/
public function update_event_layout() {
$post_id = tribe_get_request_var( 'postId' );
$layout_id = tribe_get_request_var( 'newLayout' );
if ( empty( $layout_id ) || empty( $post_id ) ) {
wp_send_json_error(
[
'error' => 'No layout ID or post ID provided',
],
400
);
return;
}
if ( ! $this->check_current_ajax_user_can( 'edit_posts', $post_id ) ) {
wp_send_json_error(
[
'error' => 'User has no permission.',
],
403
);
return;
}
$layout = DB::table( \TEC\Tickets\Seating\Tables\Layouts::table_name( false ) )->where( 'id', $layout_id )->get();
if ( empty( $layout ) ) {
wp_send_json_error(
[
'error' => 'Invalid layout ID',
],
400
);
return;
}
/** @var \Tribe__Tickets__Tickets_Handler $tickets_handler */
$tickets_handler = tribe( 'tickets.handler' );
$capacity_meta_key = $tickets_handler->key_capacity;
$primary_seat_type = $this->seat_types->get_primary_seat_type( $layout_id );
if ( null === $primary_seat_type ) {
wp_send_json_error(
[
'error' => __( 'No primary seat type found for the layout.', 'event-tickets' ),
],
400
);
return;
}
$new_seat_type_id = $primary_seat_type->id;
$new_seat_capacity = $primary_seat_type->seats;
$updated_tickets = 0;
$updated_attendees = 0;
// Get tickets by post id.
$tickets = tribe_tickets()->where( 'event', $post_id )->get_ids( true );
// We're handling the update of the ticket meta ourselves.
remove_filter( 'update_post_metadata', [ tribe( Controller::class ), 'handle_ticket_meta_update' ], 10 );
foreach ( $tickets as $ticket_id ) {
$previous_capacity = get_post_meta( $ticket_id, $capacity_meta_key, true );
$capacity_delta = $new_seat_capacity - $previous_capacity;
$previous_stock = get_post_meta( $ticket_id, '_stock', true );
$new_stock = max( 0, $previous_stock + $capacity_delta );
update_post_meta( $ticket_id, $capacity_meta_key, $new_seat_capacity );
update_post_meta( $ticket_id, '_stock', $new_stock );
update_post_meta( $ticket_id, Meta::META_KEY_SEAT_TYPE, $new_seat_type_id );
++$updated_tickets;
}
// Attendees by post id.
$attendees = tribe_attendees()->where( 'event', $post_id )->get_ids( true );
foreach ( $attendees as $attendee_id ) {
update_post_meta( $attendee_id, Meta::META_KEY_SEAT_TYPE, $new_seat_type_id );
update_post_meta( $attendee_id, Meta::META_KEY_ATTENDEE_SEAT_LABEL, '' );
++$updated_attendees;
}
// Finally update post data.
update_post_meta( $post_id, Meta::META_KEY_LAYOUT_ID, $layout_id );
update_post_meta( $post_id, $capacity_meta_key, $layout->seats );
update_post_meta( $post_id, Global_Stock::GLOBAL_STOCK_LEVEL, $layout->seats );
add_filter( 'update_post_metadata', [ tribe( Controller::class ), 'handle_ticket_meta_update' ], 10, 4 );
wp_send_json_success(
[
'updatedTickets' => $updated_tickets,
'updatedAttendees' => $updated_attendees,
]
);
}
/**
* Removes the layout from an event.
*
* @since 5.18.0
*
* @return void The function does not return a value but will echo the JSON response.
*/
public function remove_event_layout() {
$post_id = tribe_get_request_var( 'postId' );
if ( empty( $post_id ) ) {
wp_send_json_error(
[
'error' => __( 'No post ID provided', 'event-tickets' ),
],
400
);
return;
}
if ( ! $this->check_current_ajax_user_can( 'edit_posts', $post_id ) ) {
wp_send_json_error(
[
'error' => __( 'User has no permission.', 'event-tickets' ),
],
403
);
return;
}
$layout_id = get_post_meta( $post_id, Meta::META_KEY_LAYOUT_ID, true );
if ( empty( $layout_id ) ) {
wp_send_json_error(
[
'error' => __( 'Layout not found.', 'event-tickets' ),
],
403
);
return;
}
$updated_tickets = 0;
$updated_attendees = 0;
// Get tickets by post id.
$tickets = tribe_tickets()
->where( 'event', $post_id )
->where( 'meta_exists', Meta::META_KEY_SEAT_TYPE )
->get_ids( true );
foreach ( $tickets as $ticket_id ) {
// Remove slr meta.
delete_post_meta( $ticket_id, Meta::META_KEY_ENABLED );
delete_post_meta( $ticket_id, Meta::META_KEY_LAYOUT_ID );
delete_post_meta( $ticket_id, Meta::META_KEY_SEAT_TYPE );
// Switch ticket to own stock mode.
update_post_meta( $ticket_id, Global_Stock::TICKET_STOCK_MODE, Global_Stock::OWN_STOCK_MODE );
// Set ticket capacity to 1.
tribe_tickets_delete_capacity( $ticket_id );
tribe_tickets_update_capacity( $ticket_id, 1 );
// Update ticket stock.
update_post_meta( $ticket_id, '_stock', 1 );
++$updated_tickets;
clean_post_cache( $ticket_id );
}
// Attendees by post id.
$attendees = tribe_attendees()
->where( 'event', $post_id )
->where( 'meta_equals', Meta::META_KEY_LAYOUT_ID, $layout_id )
->get_ids( true );
foreach ( $attendees as $attendee_id ) {
delete_post_meta( $attendee_id, Meta::META_KEY_SEAT_TYPE );
delete_post_meta( $attendee_id, Meta::META_KEY_ATTENDEE_SEAT_LABEL );
delete_post_meta( $attendee_id, Meta::META_KEY_LAYOUT_ID );
clean_post_cache( $attendee_id );
++$updated_attendees;
}
// Finally update post data.
delete_post_meta( $post_id, Meta::META_KEY_LAYOUT_ID );
delete_post_meta( $post_id, Meta::META_KEY_ENABLED );
// Remove global stock.
tribe_tickets_delete_capacity( $post_id );
clean_post_cache( $post_id );
wp_send_json_success(
[
'updatedTickets' => $updated_tickets,
'updatedAttendees' => $updated_attendees,
]
);
}
}
|