prefix . 'seatt_tickets'; } public static function init() { // Before the receipt email (priority 10), so it can include every QR. add_action( 'seatt_booking_paid', array( __CLASS__, 'ensure' ), 5 ); } /** Creates the ticket rows of a booking if it has none. Returns the rows. */ public static function ensure( $booking_id ) { global $wpdb; $booking_id = (int) $booking_id; $rows = self::rows( $booking_id ); if ( $rows ) { return $rows; } $b = SEATT_DB::get_booking( $booking_id ); if ( ! $b ) { return array(); } $now = current_time( 'mysql' ); $in_at = ! empty( $b->checked_in_at ) ? $b->checked_in_at : null; // legacy group check-in $in_by = $in_at && ! empty( $b->checked_in_by ) ? (int) $b->checked_in_by : null; $n = 0; $add = function ( $seat_id, $zone_id, $line, $name, $email ) use ( $wpdb, $booking_id, $now, $in_at, $in_by, &$n ) { $n++; $wpdb->insert( $wpdb->seatt_tickets, array( 'booking_id' => $booking_id, 'ticket_no' => $n, 'seat_id' => $seat_id ?: null, 'zone_id' => $zone_id ?: null, 'line_no' => $line, 'holder_name' => mb_substr( (string) $name, 0, 191 ), 'holder_email' => $email ? mb_substr( (string) $email, 0, 191 ) : null, 'token' => wp_generate_password( 32, false, false ), 'checked_in_at' => $in_at, 'checked_in_by' => $in_by, 'created_at' => $now, ) ); }; $seats = $wpdb->get_results( $wpdb->prepare( 'SELECT seat_id, attendee_name, attendee_email FROM %i WHERE booking_id = %d ORDER BY id', $wpdb->seatt_booking_seats, $booking_id ) ); foreach ( (array) $seats as $s ) { $add( (int) $s->seat_id, 0, 1, $s->attendee_name ?: $b->buyer_name, $s->attendee_email ?: ( $s->attendee_name && 0 !== strcasecmp( $s->attendee_name, $b->buyer_name ) ? '' : $b->buyer_email ) ); } $zones = $wpdb->get_results( $wpdb->prepare( 'SELECT zone_id, quantity FROM %i WHERE booking_id = %d ORDER BY id', $wpdb->seatt_booking_zones, $booking_id ) ); foreach ( (array) $zones as $z ) { $holders = array(); if ( class_exists( 'SEATT_Attendees' ) ) { foreach ( (array) $wpdb->get_results( $wpdb->prepare( 'SELECT line_no, name, email FROM %i WHERE booking_id = %d AND zone_id = %d', $wpdb->seatt_booking_attendees, $booking_id, $z->zone_id ) ) as $h ) { $holders[ (int) $h->line_no ] = $h; } } for ( $i = 1; $i <= (int) $z->quantity; $i++ ) { $h = $holders[ $i ] ?? null; $add( 0, (int) $z->zone_id, $i, $h ? $h->name : $b->buyer_name, $h ? $h->email : $b->buyer_email ); } } return self::rows( $booking_id ); } /** Ticket rows with a readable label ("A7" / "Regular #2"). */ public static function rows( $booking_id ) { global $wpdb; $rows = $wpdb->get_results( $wpdb->prepare( 'SELECT t.*, s.row_name, s.label AS seat_label, s.category AS seat_category, z.name AS zone_name FROM %i t LEFT JOIN %i s ON s.id = t.seat_id LEFT JOIN %i z ON z.id = t.zone_id WHERE t.booking_id = %d ORDER BY t.ticket_no', $wpdb->seatt_tickets, $wpdb->seatt_seats, $wpdb->seatt_zones, (int) $booking_id ) ); foreach ( (array) $rows as $r ) { $r->label = $r->seat_id ? seatt_seat_label( $r->row_name, $r->seat_label ) : trim( (string) $r->zone_name ) . ' #' . (int) $r->line_no; } return (array) $rows; } /** Every paid booking of an event gets its rows (for scanners and stats). */ public static function ensure_event( $event_id ) { global $wpdb; $ids = $wpdb->get_col( $wpdb->prepare( 'SELECT b.id FROM %i b LEFT JOIN %i t ON t.booking_id = b.id WHERE b.event_id = %d AND b.status = \'paid\' AND t.id IS NULL', $wpdb->seatt_bookings, $wpdb->seatt_tickets, (int) $event_id ) ); foreach ( (array) $ids as $id ) { self::ensure( (int) $id ); } } /** The ticket a QR points to, or null (token compared in constant time). */ public static function find( $booking_id, $ticket_id, $token ) { global $wpdb; $row = $wpdb->get_row( $wpdb->prepare( 'SELECT * FROM %i WHERE id = %d AND booking_id = %d', $wpdb->seatt_tickets, (int) $ticket_id, (int) $booking_id ) ); return ( $row && '' !== (string) $token && hash_equals( (string) $row->token, (string) $token ) ) ? $row : null; } public static function check_in( $ticket_id, $user_id, $at = null ) { global $wpdb; $wpdb->query( $wpdb->prepare( 'UPDATE %i SET checked_in_at = %s, checked_in_by = NULLIF( %d, 0 ) WHERE id = %d AND checked_in_at IS NULL', $wpdb->seatt_tickets, $at ?: current_time( 'mysql' ), (int) $user_id, (int) $ticket_id ) ); } /** The booking's own (group) QR or "Mark arrived": every ticket not yet in. */ public static function check_in_all( $booking_id, $user_id, $at = null ) { global $wpdb; self::ensure( $booking_id ); $wpdb->query( $wpdb->prepare( 'UPDATE %i SET checked_in_at = %s, checked_in_by = NULLIF( %d, 0 ) WHERE booking_id = %d AND checked_in_at IS NULL', $wpdb->seatt_tickets, $at ?: current_time( 'mysql' ), (int) $user_id, (int) $booking_id ) ); } /** Undo (the admin "undo check-in" action): the whole booking. */ public static function reset( $booking_id ) { global $wpdb; $wpdb->query( $wpdb->prepare( 'UPDATE %i SET checked_in_at = NULL, checked_in_by = NULL WHERE booking_id = %d', $wpdb->seatt_tickets, (int) $booking_id ) ); } /** Door-scan URL of one ticket. */ public static function url( $ticket, $page_base ) { return add_query_arg( array( 'seatt_checkin_id' => (int) $ticket->booking_id, 'seatt_checkin_token' => $ticket->token, 'seatt_t' => (int) $ticket->id ), $page_base ); } /** "2 / 4" arrival count of a booking. */ public static function progress( $booking_id ) { global $wpdb; $r = $wpdb->get_row( $wpdb->prepare( 'SELECT COUNT(*) AS n, SUM(checked_in_at IS NOT NULL) AS a FROM %i WHERE booking_id = %d', $wpdb->seatt_tickets, (int) $booking_id ) ); return array( 'arrived' => (int) ( $r->a ?? 0 ), 'total' => (int) ( $r->n ?? 0 ) ); } }