/** * Plugin Name: Event Ticket Totals (custom) * Description: Returns event-level totals (capacity, sold, available) for events in a category by inspecting Tribe tickets and WooCommerce orders. * Version: 1.0 * Author: ChatGPT (example) */ add_action( 'rest_api_init', function() { register_rest_route( 'greathall/v1', '/event-totals', array( 'methods' => 'GET', 'callback' => 'greathall_event_totals_with_events_handler_cached', 'permission_callback' => function () { return true; }, // adjust as needed ) ); }, 9 ); // run before some plugins that hook into rest function greathall_set_cors_headers_for_request() { $allowed_origins = array( 'http://localhost:3000', 'https://greathalltheatrical.com', ); $origin = isset( $_SERVER['HTTP_ORIGIN'] ) ? trim( wp_unslash( $_SERVER['HTTP_ORIGIN'] ) ) : ''; if ( in_array( $origin, $allowed_origins, true ) ) { header( 'Access-Control-Allow-Origin: ' . esc_url_raw( $origin ) ); header( 'Access-Control-Allow-Credentials: true' ); } header( 'Access-Control-Allow-Methods: GET, POST, OPTIONS' ); header( 'Access-Control-Allow-Headers: Authorization, Content-Type, X-WP-Nonce, X-Requested-With' ); header( 'Access-Control-Expose-Headers: X-WP-Total, X-WP-TotalPages, Link' ); // quick respond to OPTIONS preflight on this route if ( isset( $_SERVER['REQUEST_METHOD'] ) && 'OPTIONS' === strtoupper( $_SERVER['REQUEST_METHOD'] ) ) { http_response_code(200); echo ''; exit; } } function greathall_parse_woo_product_id( $global_id ) { if ( empty( $global_id ) ) return null; if ( preg_match( '/[?&]id=(\d+)/', $global_id, $m ) ) return intval( $m[1] ); if ( preg_match( '/(\d+)$/', $global_id, $m2 ) ) return intval( $m2[1] ); return null; } function greathall_prices_from_ticket_details( $ticket_details ) { $regular_low = null; $regular_high = null; $current_low = null; $current_high = null; $to_float = function( $v ) { if ( $v === null ) return null; if ( $v === '' ) return null; if ( is_string( $v ) ) { $v = trim( $v ); if ( $v === '' ) return null; } return is_numeric( $v ) ? floatval( $v ) : null; }; $update = function( &$min, &$max, $val ) { if ( $val === null ) return; if ( $min === null || $val < $min ) $min = $val; if ( $max === null || $val > $max ) $max = $val; }; if ( ! is_array( $ticket_details ) ) { $ticket_details = array(); } foreach ( $ticket_details as $td ) { $pi = isset( $td['price_info'] ) ? $td['price_info'] : null; if ( ! is_array( $pi ) ) continue; $price = array_key_exists( 'price', $pi ) ? $to_float( $pi['price'] ) : null; $regular = array_key_exists( 'regular_price', $pi ) ? $to_float( $pi['regular_price'] ) : null; $sale = array_key_exists( 'sale_price', $pi ) ? $to_float( $pi['sale_price'] ) : null; if ( $regular === null ) $regular = $price; $is_on_sale = ! empty( $pi['is_on_sale'] ); $current = null; if ( $is_on_sale && $sale !== null ) { $current = $sale; } if ( $current === null ) $current = $price; if ( $current === null ) $current = $regular; $update( $regular_low, $regular_high, $regular ); $update( $current_low, $current_high, $current ); } return array( 'regular' => array( 'low' => $regular_low, 'high' => $regular_high ), 'current' => array( 'low' => $current_low, 'high' => $current_high ), ); } /** * Compute totals (capacity, sold, available) for a list of event IDs, * and include per-ticket product price info (regular, sale, current). * * Returns an associative array: * [ event_id => [ * 'total_capacity' => int|null, * 'total_sold' => int, * 'total_available' => int|null, * 'tickets_count' => int, * 'ticket_details' => [ { ticket_id, provider, capacity, product_id, sold, price_info }, ... ], * 'prices' => [ * 'regular' => [ 'low' => float|null, 'high' => float|null ], * 'current' => [ 'low' => float|null, 'high' => float|null ], * ], * ] * ] */ function greathall_compute_totals_for_event_ids( $event_ids = array() ) { global $wpdb; if ( empty( $event_ids ) || ! is_array( $event_ids ) ) { return array(); } // Normalize and unique event IDs $event_ids = array_map( 'intval', $event_ids ); $event_ids = array_values( array_filter( array_unique( $event_ids ), function( $v ) { return $v > 0; } ) ); if ( empty( $event_ids ) ) return array(); // Helper: parse product id from global_id string $parse_product_id = function( $global_id ) { if ( empty( $global_id ) ) return null; if ( preg_match( '/[?&]id=(\d+)/', $global_id, $m ) ) return intval( $m[1] ); if ( preg_match( '/(\d+)$/', $global_id, $m2 ) ) return intval( $m2[1] ); return null; }; // Helper: normalize numeric values to float or null $to_float_or_null = function( $v ) { if ( $v === null ) return null; if ( $v === '' ) return null; if ( is_string( $v ) ) { $v = trim( $v ); if ( $v === '' ) return null; } if ( ! is_numeric( $v ) ) return null; return floatval( $v ); }; // Helper: update min/max $update_minmax = function( &$min, &$max, $val ) { if ( $val === null ) return; if ( $min === null || $val < $min ) $min = $val; if ( $max === null || $val > $max ) $max = $val; }; // Map to hold per-event ticket info and collect product ids $event_ticket_map = array(); // event_id => [ ticket arrays... ] $all_product_ids = array(); // product_id => true (set) // For each event, call the internal tribe tickets route to get tickets foreach ( $event_ids as $eid ) { $route = '/tribe/tickets/v1/tickets'; $req = new WP_REST_Request( 'GET', $route ); $req->set_query_params( array( 'include_post' => $eid, 'per_page' => 100 ) ); $resp = rest_do_request( $req ); $tickets = array(); if ( ! $resp->is_error() ) { $data = $resp->get_data(); if ( isset( $data['tickets'] ) && is_array( $data['tickets'] ) ) { $tickets = $data['tickets']; } elseif ( is_array( $data ) ) { $tickets = $data; } } $event_ticket_map[ $eid ] = array(); foreach ( $tickets as $t ) { $tid = isset( $t['id'] ) ? intval( $t['id'] ) : null; $provider = isset( $t['provider'] ) ? $t['provider'] : ''; $capacity = isset( $t['capacity'] ) ? intval( $t['capacity'] ) : null; $global_id = isset( $t['global_id'] ) ? $t['global_id'] : ''; $product_id = null; if ( $provider === 'woo' ) { // In TEC Woo tickets, ticket "id" should be the Woo product ID. $candidate = $tid ? intval( $tid ) : null; // Sanity-check: confirm it's a real WC product (or variation). If not, fall back to global_id parsing. $is_product = false; if ( $candidate ) { $pt = get_post_type( $candidate ); if ( $pt === 'product' || $pt === 'product_variation' ) { $is_product = true; } } if ( $is_product ) { $product_id = $candidate; } else { // Fallback only if TEC is returning a non-product ticket ID for some reason. $product_id = $parse_product_id( $global_id ); } if ( $product_id ) { $all_product_ids[ $product_id ] = true; } } $event_ticket_map[ $eid ][] = array( 'ticket_id' => $tid, 'provider' => $provider, 'capacity' => $capacity, 'product_id' => $product_id, ); } } // Build list of product IDs (integers) $product_ids = array_map( 'intval', array_keys( $all_product_ids ) ); $product_sold_counts = array(); // product_id => total sold qty if ( ! empty( $product_ids ) ) { // Prepare placeholders for the IN clause $placeholders = implode( ',', array_fill( 0, count( $product_ids ), '%d' ) ); // Table names with prefix $prefix = $wpdb->prefix; $order_items_table = $prefix . 'woocommerce_order_items'; $order_itemmeta_table = $prefix . 'woocommerce_order_itemmeta'; $posts_table = $prefix . 'posts'; // SQL: sum _qty grouped by _product_id for shop_order with desired statuses $sql = " SELECT pm2.meta_value AS product_id, SUM( CAST(pm1.meta_value AS UNSIGNED) ) AS total_qty FROM {$order_items_table} AS oi INNER JOIN {$order_itemmeta_table} AS pm1 ON oi.order_item_id = pm1.order_item_id AND pm1.meta_key = '_qty' INNER JOIN {$order_itemmeta_table} AS pm2 ON oi.order_item_id = pm2.order_item_id AND pm2.meta_key = '_product_id' INNER JOIN {$posts_table} AS p ON oi.order_id = p.ID WHERE p.post_type = 'shop_order' AND p.post_status IN ( 'wc-completed', 'wc-processing' ) AND pm2.meta_value IN ( {$placeholders} ) GROUP BY pm2.meta_value "; // Build prepare args (first param is SQL string) $prepare_args = array_merge( array( $sql ), $product_ids ); $prepared = call_user_func_array( array( $wpdb, 'prepare' ), $prepare_args ); $rows = $wpdb->get_results( $prepared ); if ( $rows ) { foreach ( $rows as $r ) { $pid = intval( $r->product_id ); $qty = intval( $r->total_qty ); $product_sold_counts[ $pid ] = $qty; } } } // Preload product price info using wc_get_product (if available) $product_price_info = array(); // product_id => [ price, regular_price, sale_price, is_on_sale ] if ( function_exists( 'wc_get_product' ) && ! empty( $product_ids ) ) { foreach ( $product_ids as $pid ) { $prod = wc_get_product( $pid ); if ( $prod ) { $product_price_info[ $pid ] = array( 'price' => $prod->get_price(), // current effective price (string) 'regular_price' => $prod->get_regular_price(), // string or '' 'sale_price' => $prod->get_sale_price(), // string or '' 'is_on_sale' => $prod->is_on_sale() ? true : false, ); } else { $product_price_info[ $pid ] = array( 'price' => null, 'regular_price' => null, 'sale_price' => null, 'is_on_sale' => false, ); } } } // Now compute totals for each event using its tickets and the product_sold_counts $results = array(); foreach ( $event_ticket_map as $eid => $tickets ) { // Deduplicate tickets by ticket_id if provided $seen = array(); $unique = array(); foreach ( $tickets as $t ) { if ( $t['ticket_id'] && isset( $seen[ $t['ticket_id'] ] ) ) continue; if ( $t['ticket_id'] ) $seen[ $t['ticket_id'] ] = true; $unique[] = $t; } $total_capacity = 0; $found_positive_capacity = false; $total_sold = 0; // Price ranges (per event) $regular_low = null; $regular_high = null; $current_low = null; $current_high = null; // Build ticket_details to include price info $ticket_details = array(); foreach ( $unique as $t ) { $cap = $t['capacity']; if ( $cap && $cap > 0 ) { $found_positive_capacity = true; $total_capacity += intval( $cap ); } $ticket_product_sold = 0; $price_info = null; if ( $t['provider'] === 'woo' && $t['product_id'] ) { $pid = intval( $t['product_id'] ); $ticket_product_sold = isset( $product_sold_counts[ $pid ] ) ? intval( $product_sold_counts[ $pid ] ) : 0; $price_info = isset( $product_price_info[ $pid ] ) ? $product_price_info[ $pid ] : null; // Update price ranges from this ticket if ( is_array( $price_info ) ) { // Regular/original price: prefer regular_price, else fallback to price $regular = null; if ( array_key_exists( 'regular_price', $price_info ) ) { $regular = $to_float_or_null( $price_info['regular_price'] ); } if ( $regular === null && array_key_exists( 'price', $price_info ) ) { $regular = $to_float_or_null( $price_info['price'] ); } // Current/effective price: prefer sale_price if on sale, else price, else regular $current = null; $is_on_sale = ! empty( $price_info['is_on_sale'] ); if ( $is_on_sale && array_key_exists( 'sale_price', $price_info ) ) { $current = $to_float_or_null( $price_info['sale_price'] ); } if ( $current === null && array_key_exists( 'price', $price_info ) ) { $current = $to_float_or_null( $price_info['price'] ); } if ( $current === null ) { $current = $regular; } $update_minmax( $regular_low, $regular_high, $regular ); $update_minmax( $current_low, $current_high, $current ); } } $total_sold += $ticket_product_sold; $ticket_details[] = array( 'ticket_id' => $t['ticket_id'], 'provider' => $t['provider'], 'capacity' => $t['capacity'], 'product_id' => $t['product_id'], 'sold' => $ticket_product_sold, 'price_info' => $price_info, ); } if ( ! $found_positive_capacity ) { $total_capacity = null; } $total_available = ( $total_capacity !== null ) ? max( 0, (int) $total_capacity - (int) $total_sold ) : null; $results[ $eid ] = array( 'total_capacity' => $total_capacity, 'total_sold' => $total_sold, 'total_available' => $total_available, 'tickets_count' => count( $unique ), 'ticket_details' => $ticket_details, 'prices' => array( 'regular' => array( 'low' => $regular_low, 'high' => $regular_high, ), 'current' => array( 'low' => $current_low, 'high' => $current_high, ), ), ); } return $results; } /* ------------------------------------------------------------ Greathall: merged events + ticket totals endpoint with caching - returns full tribe event objects with ticketTotals (and price_info) - caches per-category result in a transient - invalidates cache on product/order/ticket changes ------------------------------------------------------------ */ /** * Handler: returns merged events with ticket totals & price details. * Uses a per-category transient cache. */ function greathall_event_totals_with_events_handler_cached( $request ) { // allow CORS for dev/prod origins (reuse existing function) if ( function_exists( 'greathall_set_cors_headers_for_request' ) ) { greathall_set_cors_headers_for_request(); } $params = $request->get_params(); $cat_id = isset( $params['category'] ) ? intval( $params['category'] ) : 0; if ( ! $cat_id ) { return new WP_Error( 'missing_category', 'Please provide category query param: ?category=ID', array( 'status' => 400 ) ); } // cache key and TTL (seconds) $transient_key = 'ght_event_totals_cat_' . $cat_id; $cache_ttl = 300; // 5 minutes — adjust as you like // return cached if present $cached = get_transient( $transient_key ); if ( false !== $cached ) { return rest_ensure_response( $cached ); } // 1) fetch tribe events internally $route = '/tribe/events/v1/events'; $evReq = new WP_REST_Request( 'GET', $route ); $evReq->set_query_params( array( 'categories' => $cat_id, 'per_page' => 100 ) ); $evResp = rest_do_request( $evReq ); if ( $evResp->is_error() ) { return rest_ensure_response( array( 'error' => 'Failed to get tribe events' ) ); } $eventsData = $evResp->get_data(); // normalize shapes if ( isset( $eventsData['events'] ) && is_array( $eventsData['events'] ) ) { $events = $eventsData['events']; } elseif ( is_array( $eventsData ) ) { $events = $eventsData; } else { $events = array(); } // collect event ids $event_ids = array(); foreach ( $events as $e ) { $eid = isset( $e['id'] ) ? intval( $e['id'] ) : null; if ( $eid ) $event_ids[] = $eid; } $event_ids = array_values( array_unique( $event_ids ) ); // 2) compute totals map using your fast function // greathall_compute_totals_for_event_ids() should return per-event arrays if ( function_exists( 'greathall_compute_totals_for_event_ids' ) ) { $totals_map = greathall_compute_totals_for_event_ids( $event_ids ); } else { $totals_map = array(); } // 3) merge and compute a small price summary per event $merged = array(); foreach ( $events as $ev ) { $id = isset( $ev['id'] ) ? intval( $ev['id'] ) : null; $ticketTotals = isset( $totals_map[ $id ] ) ? $totals_map[ $id ] : null; if ( is_array( $ticketTotals ) ) { $tds = isset( $ticketTotals['ticket_details'] ) ? $ticketTotals['ticket_details'] : array(); $ticketTotals['prices'] = greathall_prices_from_ticket_details( $tds ); } $ev['ticketTotals'] = $ticketTotals; // Optional: also inject ticketPrices so the client always has it if ( is_array( $ticketTotals ) && isset( $ticketTotals['prices'] ) ) { $ev['ticketPrices'] = array( 'prices' => $ticketTotals['prices'] ); } // Build price summary from ticket_details if present $price_summary = null; if ( is_array( $ticketTotals ) && ! empty( $ticketTotals['ticket_details'] ) ) { $min_sale = null; $max_regular = null; foreach ( $ticketTotals['ticket_details'] as $td ) { $pi = isset( $td['price_info'] ) ? $td['price_info'] : null; if ( is_array( $pi ) ) { // use numeric floats if possible $sale = isset( $pi['sale_price'] ) && $pi['sale_price'] !== '' ? floatval( $pi['sale_price'] ) : null; $regular = isset( $pi['regular_price'] ) && $pi['regular_price'] !== '' ? floatval( $pi['regular_price'] ) : null; if ( $sale !== null ) { if ( $min_sale === null || $sale < $min_sale ) $min_sale = $sale; } if ( $regular !== null ) { if ( $max_regular === null || $regular > $max_regular ) $max_regular = $regular; } } } $price_summary = array( 'min_sale_price' => $min_sale, 'max_regular_price'=> $max_regular, ); } // attach to event object (nest to avoid collisions) $ev['ticketTotals'] = $ticketTotals; $ev['priceSummary'] = $price_summary; $merged[] = $ev; } // 4) cache and return greathall_build_and_save_mapping_from_merged( $merged ); set_transient( $transient_key, $merged, $cache_ttl ); return rest_ensure_response( $merged ); } /* ------------------------------------------------------------------ Surgical cache invalidation for greathall event totals (per-category) - Maintains a mapping option to know which categories are affected by each product/event - Invalidates only the affected category transients on changes ------------------------------------------------------------------ */ /** * Save mapping data used for targeted invalidation. * Structure saved in option 'ght_cache_map': * [ * 'product_to_categories' => [ product_id => [cat_id, ...], ... ], * 'event_to_categories' => [ event_id => [cat_id, ...], ... ], * 'updated' => timestamp * ] */ function greathall_save_cache_map( $map ) { if ( ! is_array( $map ) ) $map = array(); update_option( 'ght_cache_map', $map, true ); } /** * Get the mapping option, or empty array. */ function greathall_get_cache_map() { $map = get_option( 'ght_cache_map', array() ); if ( ! is_array( $map ) ) $map = array(); return $map; } /** * Delete category transients for the provided category IDs. */ function greathall_clear_transients_for_categories( $category_ids = array() ) { if ( empty( $category_ids ) || ! is_array( $category_ids ) ) return; foreach ( $category_ids as $cat_id ) { $cat_id = intval( $cat_id ); if ( $cat_id <= 0 ) continue; $key = 'ght_event_totals_cat_' . $cat_id; delete_transient( $key ); } } /** * Fallback: delete all ght_event_totals_cat_* transients (used if mapping missing) */ function greathall_clear_all_event_totals_cache() { global $wpdb; $opts = $wpdb->options; $like_values = $wpdb->esc_like( '_transient_ght_event_totals_cat_' ) . '%'; $like_timeouts = $wpdb->esc_like( '_transient_timeout_ght_event_totals_cat_' ) . '%'; $wpdb->query( $wpdb->prepare( "DELETE FROM {$opts} WHERE option_name LIKE %s OR option_name LIKE %s", $like_values, $like_timeouts ) ); } /** * Utility: add categories to map arrays (ensures uniqueness) */ function greathall_map_add_categories( &$map_array, $key_id, $categories ) { if ( empty( $categories ) ) return; if ( ! isset( $map_array[ $key_id ] ) || ! is_array( $map_array[ $key_id ] ) ) { $map_array[ $key_id ] = array(); } foreach ( $categories as $c ) { $cid = intval( $c ); if ( $cid && ! in_array( $cid, $map_array[ $key_id ], true ) ) { $map_array[ $key_id ][] = $cid; } } } /** * Rebuild mapping for a category result set when we generate (or refresh) it. * Call this inside your cached handler right *before* set_transient($transient_key, $merged, $cache_ttl) * so the mapping reflects the data just cached. * * Expects $merged: array of event objects returned to client (each with id and ticketTotals.ticket_details) */ function greathall_build_and_save_mapping_from_merged( $merged ) { // structure $map = array( 'product_to_categories' => array(), 'event_to_categories' => array(), 'updated' => time(), ); foreach ( $merged as $ev ) { $eid = isset( $ev['id'] ) ? intval( $ev['id'] ) : null; // tribe events payload usually has categories as array of { id, name, ... } — be defensive $cats = array(); if ( isset( $ev['categories'] ) && is_array( $ev['categories'] ) ) { foreach ( $ev['categories'] as $c ) { if ( is_array( $c ) && isset( $c['id'] ) ) $cats[] = intval( $c['id'] ); elseif ( is_numeric( $c ) ) $cats[] = intval( $c ); } } if ( $eid ) { greathall_map_add_categories( $map['event_to_categories'], $eid, $cats ); } // tickets may be nested under ticketTotals.ticket_details $tds = isset( $ev['ticketTotals']['ticket_details'] ) ? $ev['ticketTotals']['ticket_details'] : array(); if ( is_array( $tds ) && ! empty( $tds ) ) { foreach ( $tds as $td ) { $pid = isset( $td['product_id'] ) ? intval( $td['product_id'] ) : null; if ( $pid ) { greathall_map_add_categories( $map['product_to_categories'], $pid, $cats ); } } } } greathall_save_cache_map( $map ); } /* ------------------------------------------------------------------ Targeted invalidation hooks - product save/update - product (woocommerce_update_product) - order status changes (invalidate categories touched by order's products) - ticket/event saves ------------------------------------------------------------------ */ /** * When a product is updated (save_post), clear only categories associated with that product. * Use the mapping to find categories. */ add_action( 'save_post', function( $post_id, $post, $update ) { if ( $post->post_type !== 'product' ) return; $pid = intval( $post_id ); $map = greathall_get_cache_map(); if ( empty( $map['product_to_categories'] ) ) { greathall_clear_all_event_totals_cache(); return; } if ( isset( $map['product_to_categories'][ $pid ] ) ) { greathall_clear_transients_for_categories( $map['product_to_categories'][ $pid ] ); } else { // If product not in map, safe fallback: do nothing or clear all (choose fallback) // We'll clear nothing to avoid heavy churn. If you prefer safety, uncomment next line: // greathall_clear_all_event_totals_cache(); } }, 20, 3 ); /** * Woo hook (extra safety) when product updated */ add_action( 'woocommerce_update_product', function( $product_id ) { $map = greathall_get_cache_map(); if ( ! empty( $map['product_to_categories'][ $product_id ] ) ) { greathall_clear_transients_for_categories( $map['product_to_categories'][ $product_id ] ); } } ); /** * When an order changes status, clear categories for products in that order. */ add_action( 'woocommerce_order_status_changed', function( $order_id, $old_status, $new_status ) { try { $order = wc_get_order( $order_id ); if ( ! $order ) return; $product_ids = array(); foreach ( $order->get_items() as $item ) { $pid = $item->get_product_id(); if ( $pid ) $product_ids[] = intval( $pid ); } $product_ids = array_values( array_filter( array_unique( $product_ids ) ) ); if ( empty( $product_ids ) ) return; $map = greathall_get_cache_map(); if ( empty( $map['product_to_categories'] ) ) { greathall_clear_all_event_totals_cache(); return; } $cats_to_clear = array(); foreach ( $product_ids as $pid ) { if ( isset( $map['product_to_categories'][ $pid ] ) ) { $cats_to_clear = array_merge( $cats_to_clear, $map['product_to_categories'][ $pid ] ); } } $cats_to_clear = array_values( array_unique( array_map( 'intval', $cats_to_clear ) ) ); if ( ! empty( $cats_to_clear ) ) { greathall_clear_transients_for_categories( $cats_to_clear ); } } catch ( Exception $e ) { // fallback - if anything goes wrong, clear all greathall_clear_all_event_totals_cache(); } }, 10, 3 ); /** * When a tribe event (or ticket) is saved, clear the categories for that event only. * We'll look up the event's categories from event_to_categories map or from the event itself. */ add_action( 'save_post', function( $post_id, $post, $update ) { // handle tribe_events post saves if ( $post->post_type === 'tribe_events' ) { $eid = intval( $post_id ); $map = greathall_get_cache_map(); // If mapping knows categories for this event, clear them if ( ! empty( $map['event_to_categories'][ $eid ] ) ) { greathall_clear_transients_for_categories( $map['event_to_categories'][ $eid ] ); return; } // fallback: get categories from event's terms if possible $terms = wp_get_object_terms( $eid, 'tribe_events_cat', array( 'fields' => 'ids' ) ); if ( ! is_wp_error( $terms ) && ! empty( $terms ) ) { greathall_clear_transients_for_categories( $terms ); return; } // last resort: clear all greathall_clear_all_event_totals_cache(); } // handle ticket post saves if ticket is stored as post type (some installs) if ( $post->post_type === 'tribe_tickets' ) { // ticket -> post_id points to event id; try to retrieve $ticket_id = intval( $post_id ); // internal REST request to get ticket detail and its post_id $route = '/tribe/tickets/v1/tickets/' . $ticket_id; $req = new WP_REST_Request( 'GET', $route ); $resp = rest_do_request( $req ); if ( ! $resp->is_error() ) { $data = $resp->get_data(); $event_post_id = isset( $data['post_id'] ) ? intval( $data['post_id'] ) : null; if ( $event_post_id ) { $map = greathall_get_cache_map(); if ( ! empty( $map['event_to_categories'][ $event_post_id ] ) ) { greathall_clear_transients_for_categories( $map['event_to_categories'][ $event_post_id ] ); return; } $terms = wp_get_object_terms( $event_post_id, 'tribe_events_cat', array( 'fields' => 'ids' ) ); if ( ! is_wp_error( $terms ) && ! empty( $terms ) ) { greathall_clear_transients_for_categories( $terms ); return; } } } // fallback: greathall_clear_all_event_totals_cache(); } }, 11, 3 ); Past Events from June 20, 2025 – May 26, 2025 – Page 5 – Great Hall Theatrics
  • Take My Death Away 2025

    Angelus Theatre 165 N Main, Spanish Fork, UT, United States

    document.addEventListener('DOMContentLoaded', function () { const button = document.querySelector('button'); if (button) { setTimeout(function() { button.click(); }, 100); } });

    $18.00 – $26.00
  • A Christmas Carol 2025

    Angelus Theatre 165 N Main, Spanish Fork, UT, United States

    document.addEventListener('DOMContentLoaded', function () { const button = document.querySelector('button'); if (button) { setTimeout(function() { button.click(); }, 100); } });

    $16.00 – $24.00
  • A Krampus Karol 2025

    Angelus Theatre 165 N Main, Spanish Fork, UT, United States

    document.addEventListener('DOMContentLoaded', function () { const button = document.querySelector('button'); if (button) { setTimeout(function() { button.click(); }, 100); } });

    $16.00 – $24.00
  • A Christmas Carol 2025

    Angelus Theatre 165 N Main, Spanish Fork, UT, United States

    document.addEventListener('DOMContentLoaded', function () { const button = document.querySelector('button'); if (button) { setTimeout(function() { button.click(); }, 100); } });

    $16.00 – $24.00
  • A Krampus Karol 2025

    Angelus Theatre 165 N Main, Spanish Fork, UT, United States

    document.addEventListener('DOMContentLoaded', function () { const button = document.querySelector('button'); if (button) { setTimeout(function() { button.click(); }, 100); } });

    $18.00 – $26.00
  • A Christmas Carol 2025

    Angelus Theatre 165 N Main, Spanish Fork, UT, United States

    document.addEventListener('DOMContentLoaded', function () { const button = document.querySelector('button'); if (button) { setTimeout(function() { button.click(); }, 100); } });

    $16.00 – $24.00
  • A Krampus Karol 2025

    Angelus Theatre 165 N Main, Spanish Fork, UT, United States

    document.addEventListener('DOMContentLoaded', function () { const button = document.querySelector('button'); if (button) { setTimeout(function() { button.click(); }, 100); } });

    $16.00 – $24.00
  • A Christmas Carol 2025

    Angelus Theatre 165 N Main, Spanish Fork, UT, United States

    document.addEventListener('DOMContentLoaded', function () { const button = document.querySelector('button'); if (button) { setTimeout(function() { button.click(); }, 100); } });

    $16.00 – $24.00
  • A Krampus Karol 2025

    Angelus Theatre 165 N Main, Spanish Fork, UT, United States

    document.addEventListener('DOMContentLoaded', function () { const button = document.querySelector('button'); if (button) { setTimeout(function() { button.click(); }, 100); } });

    $16.00 – $24.00
  • A Christmas Carol 2025

    Angelus Theatre 165 N Main, Spanish Fork, UT, United States

    document.addEventListener('DOMContentLoaded', function () { const button = document.querySelector('button'); if (button) { setTimeout(function() { button.click(); }, 100); } });

    $16.00 – $24.00