<?php
namespace TEC\Tickets\Commerce;
use WP_Post;
use TEC\Tickets\Commerce\Status\Denied;
use TEC\Tickets\Commerce\Status\Pending;
use TEC\Tickets\Commerce\Status\Status_Handler;
use TEC\Tickets\Commerce\Status\Status_Interface;
use TEC\Tickets\Commerce\Traits\Is_Ticket;
use TEC\Tickets\Commerce\Utils\Value;
use Tribe__Tickets__Global_Stock as Event_Stock;
use Tribe__Utils__Array as Arr;
use Tribe__Date_Utils as Date_Utils;
use Tribe__Tickets__Ticket_Object as Ticket_Object;
use TEC\Tickets\Ticket_Data;
/**
* Class Ticket.
*
* @since 5.1.9
* @since 5.24.0 extend Ticket_Data
*
* @package TEC\Tickets\Commerce
*/
class Ticket extends Ticket_Data {
use Is_Ticket;
/**
* Tickets Commerce Ticket Post Type slug.
*
* @since 5.1.9
*
* @var string
*/
const POSTTYPE = 'tec_tc_ticket';
/**
* Which meta holds the Relation ship between an ticket and which event it's registered to.
*
* @since 5.1.9
*
* @var string
*/
public static $event_relation_meta_key = '_tec_tickets_commerce_event';
/**
* Which meta holds the data for showing the ticket description.
*
* @since 5.1.9
*
* @var string
*/
public static $show_description_meta_key = '_tribe_ticket_show_description';
/**
* Which meta holds the data for the ticket start date.
*
* @since 5.26.0
*
* @var string
*/
public const START_DATE_META_KEY = '_ticket_start_date';
/**
* Which meta holds the data for the ticket end date.
*
* @since 5.26.0
*
* @var string
*/
public const END_DATE_META_KEY = '_ticket_end_date';
/**
* Which meta holds the data for the ticket start time.
*
* @since 5.26.0
*
* @var string
*/
public const START_TIME_META_KEY = '_ticket_start_time';
/**
* Which meta holds the data for the ticket end time.
*
* @since 5.1.9
*
* @var string
*/
public const END_TIME_META_KEY = '_ticket_end_time';
/**
* Which meta holds the data for the ticket sku.
*
* @since 5.1.9
*
* @var string
*/
public static $sku_meta_key = '_sku';
/**
* Which meta holds the data for the ticket price.
*
* @since 5.1.9
*
* @var string
*/
public static $price_meta_key = '_price';
/**
* Which meta holds the data for the ticket sales.
*
* @since 5.2.0
*
* @var string
*/
public static $sales_meta_key = 'total_sales';
/**
* Which meta holds the data for the ticket stock mode.
*
* @since 5.1.9
*
* @var string
*/
public static $stock_mode_meta_key = Event_Stock::TICKET_STOCK_MODE;
/**
* Which meta holds the data for the ticket stock.
*
* @since 5.1.9
*
* @var string
*/
public static $stock_meta_key = '_stock';
/**
* Which meta holds the data for the ticket stock status.
*
* @since 5.1.9
*
* @var string
*/
public static $stock_status_meta_key = '_stock_status';
/**
* Which meta holds the data for the ticket allows backorders.
*
* @since 5.1.9
*
* @var string
*/
public static $allow_backorders_meta_key = '_backorders';
/**
* Which meta holds the data for the ticket is managing stock.
*
* @since 5.1.9
*
* @var string
*/
public static $should_manage_stock_meta_key = '_manage_stock';
/**
* Prefix for the counter for a given status..
*
* @since 5.2.0
*
* @var string
*/
public static $status_count_meta_key_prefix = '_tec_tc_ticket_status_count';
/**
* The meta key that holds the ticket type.
*
* @since 5.6.7
*
* @var string
*/
public static $type_meta_key = '_type';
/**
* Stores the instance of the template engine that we will use for rendering the elements.
*
* @since 5.2.3
*
* @var \Tribe__Template
*/
protected $template;
/**
* The meta key that holds if the sale price option is enabled or not in the product editor.
*
* @since 5.9.0
*
* @var string
*/
public static $sale_price_checked_key = '_sale_price_checked';
/**
* The meta key that holds the ticket sale price.
*
* @since 5.9.0
*
* @var string
*/
public static $sale_price_key = '_sale_price';
/**
* The meta key that holds the ticket sale price start date.
*
* @since 5.9.0
*
* @var string
*/
public static $sale_price_start_date_key = '_sale_price_start_date';
/**
* The meta key that holds the ticket sale price end date.
*
* @since 5.9.0
*
* @var string
*/
public static $sale_price_end_date_key = '_sale_price_end_date';
/**
* Gets the template instance used to setup the rendering html.
*
* @since 5.2.3
*
* @return \Tribe__Template
*/
public function get_template() {
if ( empty( $this->template ) ) {
$this->template = new \Tribe__Template();
$this->template->set_template_origin( \Tribe__Tickets__Main::instance() );
$this->template->set_template_folder( 'src/views/v2/commerce/ticket' );
$this->template->set_template_context_extract( true );
$this->template->set_template_folder_lookup( true );
}
return $this->template;
}
/**
* Register this Class post type into WP.
*
* @since 5.1.9
*/
public function register_post_type() {
$post_type_args = [
'label' => __( 'Tickets', 'event-tickets' ),
'labels' => [
'name' => __( 'Tickets Commerce Tickets', 'event-tickets' ),
'singular_name' => __( 'Tickets Commerce Ticket', 'event-tickets' ),
],
'public' => false,
'show_ui' => false,
'show_in_menu' => false,
'query_var' => false,
'rewrite' => false,
'capability_type' => 'post',
'has_archive' => false,
'hierarchical' => false,
'show_in_rest' => false,
];
/**
* Filter the arguments that craft the ticket post type.
*
* @see register_post_type
*
* @since 5.1.9
*
* @param array $post_type_args Post type arguments, passed to register_post_type()
*/
$post_type_args = apply_filters( 'tec_tickets_commerce_ticket_post_type_args', $post_type_args );
register_post_type( static::POSTTYPE, $post_type_args );
}
/**
* Gets the meta Key for a given status count on a ticket.
*
* @since 5.2.0
*
* @param Status_Interface $status
*
* @return string
*/
public static function get_status_count_meta_key( Status_Interface $status ) {
return static::$status_count_meta_key_prefix . ':' . $status->get_slug();
}
/**
* Modify the counters for all the tickets involved on this particular order.
*
* @since 5.2.0
*
* @param Status_Interface $new_status New post status.
* @param Status_Interface|null $old_status Old post status.
* @param WP_Post $post Post object.
*/
public function modify_counters_by_status( $new_status, $old_status, $post ) {
$order = tec_tc_get_order( $post );
// This should never be the case, but lets be safe.
if ( empty( $order->items ) ) {
return;
}
foreach ( $order->items as $item ) {
if ( ! $this->is_ticket( $item ) ) {
continue;
}
$ticket_id = $item['ticket_id'];
$new_status_meta_key = static::get_status_count_meta_key( $new_status );
$current_new_status_qty = get_post_meta( $ticket_id, $new_status_meta_key, true );
if ( ! $current_new_status_qty ) {
$current_new_status_qty = 0;
}
update_post_meta( $ticket_id, $new_status_meta_key, (int) $current_new_status_qty + $item['quantity'] );
if ( $old_status ) {
$old_status_meta_key = static::get_status_count_meta_key( $old_status );
$current_old_status_qty = get_post_meta( $ticket_id, $old_status_meta_key, true );
if ( ! $current_old_status_qty ) {
$current_old_status_qty = 0;
}
update_post_meta( $ticket_id, $old_status_meta_key, max( 0, (int) $current_old_status_qty - $item['quantity'] ) );
}
}
}
/**
* Given a valid ticket will fetch the quantity of orders on each one of the registered status based on the counting
* that is handled by the Order status transitions system.
*
* @since 5.2.0
*
* @param int|string|\WP_Post $ticket_id Which ticket we are fetching the count for.
*
* @return array<string,int>|\WP_Error
*/
public function get_status_quantity( $ticket_id ) {
$ticket = get_post( $ticket_id );
if ( ! $ticket ) {
return new \WP_Error( 'tec-tickets-commerce-non-existent-ticket' );
}
$all_statuses = tribe( Status_Handler::class )->get_all();
$status_qty = [];
foreach ( $all_statuses as $status ) {
$value = get_post_meta( $ticket->ID, static::get_status_count_meta_key( $status ), true );
if ( empty( $value ) ) {
$value = 0;
}
$status_qty[ $status->get_slug() ] = (int) $value;
}
return $status_qty;
}
/**
* Gets an individual ticket.
*
* @todo TribeCommerceLegacy: This method needs to make use of the Ticket Model.
*
* @since 5.1.9
*
* @param int|\WP_Post $ticket_id
*
* @return null|Ticket_Object
*/
public function get_ticket( $ticket_id ) {
$ticket_id = $ticket_id instanceof \WP_Post ? $ticket_id->ID : $ticket_id;
if ( ! is_numeric( $ticket_id ) ) {
return null;
}
$cached = wp_cache_get( (int) $ticket_id, 'tec_tickets' );
if ( $cached && is_array( $cached ) ) {
return new Ticket_Object( $cached );
}
$product = get_post( $ticket_id );
if ( ! $product ) {
return null;
}
if ( static::POSTTYPE !== get_post_type( $product ) ) {
return null;
}
$event_id = get_post_meta( $ticket_id, static::$event_relation_meta_key, true );
$return = new Ticket_Object();
$return->description = $product->post_excerpt;
$return->ID = $ticket_id;
$return->name = $product->post_title;
$return->menu_order = $product->menu_order;
$return->post_type = $product->post_type;
$return->on_sale = $this->is_on_sale( $return );
$return->price = $this->get_price( $return );
$return->regular_price = $this->get_regular_price( $return->ID );
$return->value = Value::create( $return->price );
$return->provider_class = Module::class;
$return->admin_link = '';
$return->show_description = $return->show_description();
$return->start_date = get_post_meta( $ticket_id, static::START_DATE_META_KEY, true );
$return->end_date = get_post_meta( $ticket_id, static::END_DATE_META_KEY, true );
$return->start_time = get_post_meta( $ticket_id, static::START_TIME_META_KEY, true );
$return->end_time = get_post_meta( $ticket_id, static::END_TIME_META_KEY, true );
$return->sku = get_post_meta( $ticket_id, static::$sku_meta_key, true );
$qty_sold = get_post_meta( $ticket_id, static::$sales_meta_key, true );
// If the quantity sold wasn't set, default to zero
$qty_sold = $qty_sold ? $qty_sold : 0;
// Ticket stock is a simple reflection of remaining inventory for this item...
$stock = (int) get_post_meta( $ticket_id, '_stock', true );
// If we don't have a stock value, then stock should be considered 'unlimited'
if ( null === $stock ) {
$stock = - 1;
}
$return->manage_stock( tribe_is_truthy( get_post_meta( $ticket_id, '_manage_stock', true ) ) );
$return->stock( $stock );
$return->global_stock_mode( get_post_meta( $ticket_id, \Tribe__Tickets__Global_Stock::TICKET_STOCK_MODE, true ) );
$capped = get_post_meta( $ticket_id, \Tribe__Tickets__Global_Stock::TICKET_STOCK_CAP, true );
if ( '' !== $capped ) {
$return->global_stock_cap( $capped );
}
$qty_cancelled = $this->get_cancelled( $ticket_id );
// Manually add cancelled to sold so that we can remove it correctly later when calculating.
$return->qty_sold( $qty_sold + $qty_cancelled );
$return->qty_cancelled( $qty_cancelled );
$pending = $this->get_qty_pending( $ticket_id );
$return->qty_pending( $pending );
/**
* Use this Filter to change any information you want about this ticket
*
* @since 5.1.9
*
* @param object $ticket
* @param int $post_id
* @param int $ticket_id
*/
$ticket = apply_filters( 'tec_tickets_commerce_get_ticket_legacy', $return, $event_id, $ticket_id );
if ( $ticket instanceof Ticket_Object ) {
wp_cache_set( (int) $ticket->ID, $ticket->to_array(), 'tec_tickets' );
}
return $ticket;
}
/**
* Returns the total number of cancelled tickets.
*
* @todo TribeCommerceLegacy: Move this method into the another place.
*
* @since 5.1.9
* @since 5.14.0 Added the $refresh parameter and use stored status counts and utilize new memoization class.
*
* @param int $ticket_id The ticket post ID.
* @param bool $refresh Whether to try and use the cached value or not.
*
* @return int
*/
protected function get_cancelled( $ticket_id, $refresh = false ) {
// Check cache for quantities by status.
$cache = tribe_cache();
$quantities = $cache->get( 'tec_tickets_quantities_by_status_' . $ticket_id );
if ( ! is_array( $quantities ) ) {
$quantities = [];
}
if ( $refresh || ! isset( $quantities[ Denied::SLUG ] ) ) {
$denied_orders = \Tribe__Tickets__Commerce__PayPal__Order::find_by(
[
'ticket_id' => $ticket_id,
'post_status' => Denied::SLUG,
'posts_per_page' => - 1,
],
[
'items',
]
);
$denied = 0;
foreach ( $denied_orders as $denied_order ) {
$denied += $denied_order->get_item_quantity( $ticket_id );
}
$quantities[ Denied::SLUG ] = max( 0, $denied );
$cache->set( 'tec_tickets_quantities_by_status_' . $ticket_id, $quantities );
}
return $quantities[ Denied::SLUG ];
}
/**
* Returns the number of pending attendees by ticket.
*
* @todo TribeCommerceLegacy: Move this method into the another place.
*
* @since 5.1.9
* @since 5.14.0 Utilize new memoization class.
*
* @param int $ticket_id The ticket post ID
* @param bool $refresh Whether to try and use the cached value or not.
*
* @return int
*/
public function get_qty_pending( $ticket_id, $refresh = false ) {
// Check cache for quantities by status.
$cache = tribe_cache();
$quantities = $cache->get( 'tec_tickets_quantities_by_status_' . $ticket_id );
if ( ! is_array( $quantities ) ) {
$quantities = [];
}
if ( $refresh || ! isset( $quantities[ Pending::SLUG ] ) ) {
$pending_query = new \WP_Query( [
'fields' => 'ids',
'per_page' => 1,
'post_type' => Attendee::POSTTYPE,
'meta_query' => [
[
'key' => Attendee::$ticket_relation_meta_key,
'value' => $ticket_id,
],
'relation' => 'AND',
[
'key' => Attendee::$status_meta_key,
'value' => tribe( Pending::class )->get_wp_slug(),
],
],
] );
$quantities[ Pending::SLUG ] = $pending_query->found_posts;
$cache->set( 'tec_tickets_quantities_by_status_' . $ticket_id, $quantities );
}
return $quantities[ Pending::SLUG ];
}
/**
* Legacy method ported from Tribe Commerce (TPP), we are specifically avoiding refactoring anything on the first
* stage of Tickets Commerce
*
* @todo TribeCommerceLegacy: This method needs to be split into `create` and `update`
*
* @since 5.1.9
*
* @param int|WP_Post|null $post_id The post ID.
* @param Ticket_Object $ticket The ticket object.
* @param array $raw_data The raw data.
*
* @return false|int|\WP_Error
*/
public function save( $post_id, $ticket, $raw_data = [] ) {
$save_type = 'update';
if ( empty( $ticket->ID ) ) {
$save_type = 'create';
/* Create main product post */
$args = array(
'post_status' => 'publish',
'post_type' => static::POSTTYPE,
'post_author' => get_current_user_id(),
'post_excerpt' => $ticket->description,
'post_title' => $ticket->name,
'menu_order' => (int) ( $ticket->menu_order ?? tribe_get_request_var( 'menu_order', - 1 ) ),
'meta_input' => [
'_type' => $raw_data['ticket_type'] ?? 'default',
]
);
$ticket->ID = wp_insert_post( $args );
// Relate event <---> ticket
add_post_meta( $ticket->ID, static::$event_relation_meta_key, $post_id );
} else {
$args = array(
'ID' => $ticket->ID,
'post_excerpt' => $ticket->description,
'post_title' => $ticket->name,
'menu_order' => $ticket->menu_order,
'meta_input' => [
'_type' => $raw_data['ticket_type'] ?? 'default',
]
);
$ticket->ID = wp_update_post( $args );
}
if ( ! $ticket->ID ) {
return false;
}
/** @var \Tribe__Tickets__Tickets_Handler $tickets_handler */
$tickets_handler = tribe( 'tickets.handler' );
// Updates if we should show Description.
$ticket->show_description = isset( $ticket->show_description ) && tribe_is_truthy( $ticket->show_description ) ? 'yes' : 'no';
update_post_meta( $ticket->ID, $tickets_handler->key_show_description, $ticket->show_description );
// let's make sure float price values are formatted to "0.xyz"
if ( is_numeric( $ticket->price ) ) {
$ticket->price = (string) (int) $ticket->price === $ticket->price
? (int) $ticket->price
: (float) $ticket->price;
}
update_post_meta( $ticket->ID, '_price', $ticket->price );
update_post_meta( $ticket->ID, '_type', $ticket->type() ?? 'default' );
$ticket_data = \Tribe__Utils__Array::get( $raw_data, 'tribe-ticket', array() );
tribe( Module::class )->update_capacity( $ticket, $ticket_data, $save_type );
foreach ( [ 'start_date', 'start_time', 'end_date', 'end_time' ] as $time_key ) {
if ( isset( $ticket->{$time_key} ) ) {
update_post_meta( $ticket->ID, "_ticket_{$time_key}", $ticket->{$time_key} );
} else {
delete_post_meta( $ticket->ID, "_ticket_{$time_key}" );
}
}
/**
* Toggle filter to allow skipping the automatic SKU generation.
*
* @param bool $should_default_ticket_sku
*/
$should_default_ticket_sku = apply_filters( 'tribe_tickets_should_default_ticket_sku', true );
if ( $should_default_ticket_sku ) {
// make sure the SKU is set to the correct value
if ( ! empty( $raw_data['ticket_sku'] ) ) {
$sku = $raw_data['ticket_sku'];
} else {
$post_author = get_post( $ticket->ID )->post_author;
$ticket_name = $raw_data['ticket_name'] ?? $ticket->name;
$str = tribe_strtoupper( $ticket_name );
$sku = "{$ticket->ID}-{$post_author}-" . str_replace( ' ', '-', $str );
$raw_data['ticket_sku'] = $sku;
}
update_post_meta( $ticket->ID, '_sku', $sku );
}
// Fetches all Ticket Form data
$data = \Tribe__Utils__Array::get( $raw_data, 'tribe-ticket', array() );
// Fetch the Global stock Instance for this Event
$event_stock = new \Tribe__Tickets__Global_Stock( $post_id );
// Only need to do this if we haven't already set one - they shouldn't be able to edit it from here otherwise
if ( ! $event_stock->is_enabled() ) {
if ( isset( $data['event_capacity'] ) ) {
$data['event_capacity'] = trim( tec_sanitize_string( $data['event_capacity'] ) );
// If empty we need to modify to -1
if ( '' === $data['event_capacity'] ) {
$data['event_capacity'] = - 1;
}
// Makes sure it's an Int after this point
$data['event_capacity'] = (int) $data['event_capacity'];
$tickets_handler->remove_hooks();
// We need to update event post meta - if we've set a global stock
$event_stock->enable();
$event_stock->set_stock_level( $data['event_capacity'], true );
// Update Event capacity
update_post_meta( $post_id, $tickets_handler->key_capacity, $data['event_capacity'] );
update_post_meta( $post_id, $event_stock::GLOBAL_STOCK_ENABLED, 1 );
$tickets_handler->add_hooks();
}
} else {
// If the Global Stock is configured we pull it from the Event
$global_capacity = (int) tribe_tickets_get_capacity( $post_id );
$data['event_capacity'] = (int) \Tribe__Utils__Array::get( $data, 'event_capacity', 0 );
if ( ! empty( $data['event_capacity'] ) && $data['event_capacity'] !== $global_capacity ) {
// Update stock level with $data['event_capacity'].
$event_stock->set_stock_level( $data['event_capacity'], true );
} else {
// Set $data['event_capacity'] with what we know.
$data['event_capacity'] = $global_capacity;
}
}
// Default Capacity will be 0
$default_capacity = 0;
$is_capacity_passed = true;
// If we have Event Global stock we fetch that Stock
if ( $event_stock->is_enabled() ) {
$default_capacity = $data['event_capacity'];
}
// Fetch capacity field, if we don't have it use default (defined above)
$data['capacity'] = trim( \Tribe__Utils__Array::get( $data, 'capacity', $default_capacity ) );
// If empty we need to modify to the default
if ( '' !== $data['capacity'] ) {
// Makes sure it's an Int after this point
$data['capacity'] = (int) $data['capacity'];
// The only available value lower than zero is -1 which is unlimited
if ( 0 > $data['capacity'] ) {
$data['capacity'] = - 1;
}
$default_capacity = $data['capacity'];
}
// Fetch the stock if defined, otherwise use Capacity field
$data['stock'] = trim( \Tribe__Utils__Array::get( $data, 'stock', $default_capacity ) );
// If empty we need to modify to what every capacity was
if ( '' === $data['stock'] ) {
$data['stock'] = $default_capacity;
}
// Makes sure it's an Int after this point
$data['stock'] = (int) $data['stock'];
// The only available value lower than zero is -1 which is unlimited.
if ( 0 > $data['stock'] ) {
$data['stock'] = - 1;
}
$mode = isset( $data['mode'] ) ? $data['mode'] : 'own';
if ( '' !== $mode ) {
if ( 'update' === $save_type ) {
$totals = $tickets_handler->get_ticket_totals( $ticket->ID );
$data['stock'] -= $totals['pending'] + $totals['sold'];
}
// In here is safe to check because we don't have unlimited = -1
$status = ( 0 < $data['stock'] ) ? 'instock' : 'outofstock';
update_post_meta( $ticket->ID, \Tribe__Tickets__Global_Stock::TICKET_STOCK_MODE, $mode );
update_post_meta( $ticket->ID, '_stock', $data['stock'] );
update_post_meta( $ticket->ID, '_stock_status', $status );
update_post_meta( $ticket->ID, '_backorders', 'no' );
update_post_meta( $ticket->ID, '_manage_stock', 'yes' );
// Prevent Ticket Capacity from going higher then Event Capacity
if (
$event_stock->is_enabled()
&& \Tribe__Tickets__Global_Stock::OWN_STOCK_MODE !== $mode
&& (
'' === $data['capacity']
|| $data['event_capacity'] < $data['capacity']
)
) {
$data['capacity'] = $data['event_capacity'];
}
} else {
// Unlimited Tickets
// Besides setting _manage_stock to "no" we should remove the associated stock fields if set previously
update_post_meta( $ticket->ID, '_manage_stock', 'no' );
delete_post_meta( $ticket->ID, '_stock_status' );
delete_post_meta( $ticket->ID, '_stock' );
delete_post_meta( $ticket->ID, \Tribe__Tickets__Global_Stock::TICKET_STOCK_CAP );
delete_post_meta( $ticket->ID, \Tribe__Tickets__Global_Stock::TICKET_STOCK_MODE );
// Set Capacity -1 when we don't have a stock mode, which means unlimited
$data['capacity'] = - 1;
}
if ( '' !== $data['capacity'] ) {
// Update Ticket capacity
update_post_meta( $ticket->ID, $tickets_handler->key_capacity, $data['capacity'] );
}
$this->process_sale_price_data( $ticket, $raw_data );
/**
* Generic action fired after saving a ticket (by type)
*
* @since 5.2.0
*
* @param int $post_id Post ID of post the ticket is tied to
* @param Ticket_Object $ticket Ticket that was just saved
* @param array $raw_data Ticket data
* @param string $class Commerce engine class
*/
do_action( "tec_tickets_commerce_after_{$save_type}_ticket", $post_id, $ticket, $raw_data, static::class );
/**
* Generic action fired after saving a ticket.
*
* @since 5.2.0
*
* @param int $post_id Post ID of post the ticket is tied to
* @param Ticket_Object $ticket Ticket that was just saved
* @param array $raw_data Ticket data
* @param string $class Commerce engine class
*/
do_action( 'tec_tickets_commerce_after_save_ticket', $post_id, $ticket, $raw_data, static::class );
/**
* Generic action fired after saving a ticket (by type)
*
* @todo TribeCommerceLegacy
*
* @since 5.2.0
*
* @param int $post_id Post ID of post the ticket is tied to
* @param Ticket_Object $ticket Ticket that was just saved
* @param array $raw_data Ticket data
* @param string $class Commerce engine class
*/
do_action( "event_tickets_after_{$save_type}_ticket", $post_id, $ticket, $raw_data, static::class );
/**
* Generic action fired after saving a ticket.
*
* @todo TribeCommerceLegacy
*
* @since 5.2.0
*
* @param int $post_id Post ID of post the ticket is tied to
* @param Ticket_Object $ticket Ticket that was just saved
* @param array $raw_data Ticket data
* @param string $class Commerce engine class
*/
do_action( 'event_tickets_after_save_ticket', $post_id, $ticket, $raw_data, static::class );
return $ticket->ID;
}
/**
* Deletes a given ticket.
*
* @todo TribeCommerceLegacy: This method needs to be refactored to Tickets Commerce standards.
*
* @since 5.1.9
* @since 5.25.0 Removed the increment of deleted attendees count, it is handled in attendee deletion method.
*
* @param int|WP_Post|null $event_id The event ID.
* @param int|WP_Post|null $ticket_id The ticket ID.
*
* @return bool
*/
public function delete( $event_id, $ticket_id ) {
// Ensure we know the event and product IDs (the event ID may not have been passed in).
if ( empty( $event_id ) ) {
$event_id = get_post_meta( $ticket_id, Attendee::$event_relation_meta_key, true );
}
// Additional check (in case we were passed an invalid ticket ID and still can't determine the event).
if ( empty( $event_id ) ) {
return false;
}
$product_id = get_post_meta( $ticket_id, Attendee::$event_relation_meta_key, true );
// @todo: should deleting an attendee replenish a ticket stock?
// Store name so we can still show it in the attendee list.
$attendees = tribe( Module::class )->get_attendees_by_id( $event_id );
$post_to_delete = get_post( $ticket_id );
foreach ( (array) $attendees as $attendee ) {
if ( $attendee['product_id'] == $ticket_id ) {
update_post_meta( $attendee['attendee_id'], Attendee::$deleted_ticket_meta_key, esc_html( $post_to_delete->post_title ) );
}
}
// Delete the ticket/attendee post.
$delete = wp_delete_post( $ticket_id, true );
if ( is_wp_error( $delete ) || ! isset( $delete->ID ) ) {
return false;
}
do_action( 'tec_tickets_commerce_ticket_deleted', $ticket_id, $event_id, $product_id );
\Tribe__Post_Transient::instance()->delete( $event_id, \Tribe__Tickets__Tickets::ATTENDEES_CACHE );
return true;
}
/**
* Update Ticket Stock and Global Stock after deleting an Attendee.
*
* @since 5.1.9
* @since 5.5.10 updated method signature to match new action signature.
*
* @param int $attendee_id Attendee ID.
*/
public function update_stock_after_attendee_deletion( $attendee_id ) {
$event_id = (int) get_post_meta( $attendee_id, Attendee::$event_relation_meta_key, true );
$product_id = (int) get_post_meta( $attendee_id, Attendee::$ticket_relation_meta_key, true );
$global_stock = new \Tribe__Tickets__Global_Stock( $event_id );
$shared_capacity = false;
if ( $global_stock->is_enabled() ) {
$shared_capacity = true;
}
$this->decrease_ticket_sales_by( $product_id, 1, $shared_capacity, $global_stock );
$this->increase_ticket_stock_by( $product_id );
// Increase the deleted attendees count.
\Tribe__Tickets__Attendance::instance( $event_id )->increment_deleted_attendees_count();
// Update the cache.
\Tribe__Post_Transient::instance()->delete( $event_id, \Tribe__Tickets__Tickets::ATTENDEES_CACHE );
}
/**
* Update Global Stock.
*
* @todo TribeCommerceLegacy: Not sure where this method fits, might just need to integrate it it into the
* create/update methods and delete this.
*
* @since 5.1.9
*
* @param \Tribe__Tickets__Global_Stock $global_stock The global stock object.
* @param int $qty The quantity to modify stock.
* @param bool $increase Whether to increase stock, default is false.
*/
public function update_global_stock( $global_stock, $qty = 1, $increase = false ) {
$level = $global_stock->get_stock_level();
if ( $increase ) {
$new_level = (int) $level + (int) $qty;
} else {
$new_level = (int) $level - (int) $qty;
}
$global_stock->set_stock_level( $new_level );
}
/**
* Increase the sales for a ticket by a specific quantity.
*
* @todo TribeCommerceLegacy: This should be moved into using a Flag Action.
*
* @since 5.1.9
* @since 5.13.3 Modified logic when updating global stock.
*
* @param int $ticket_id The ticket post ID.
* @param int $quantity The quantity to increase the ticket sales by.
* @param bool $shared_capacity Whether the ticket is using shared capacity.
* @param \Tribe__Tickets__Global_Stock|null $global_stock The stock object or null.
*
* @return int The new sales amount.
*/
public function increase_ticket_sales_by( $ticket_id, $quantity = 1, $shared_capacity = false, $global_stock = null ) {
$original_total_sales = (int) get_post_meta(
$ticket_id,
static::$sales_meta_key,
true
);
$updated_total_sales = $quantity + $original_total_sales;
update_post_meta(
$ticket_id,
static::$sales_meta_key,
$updated_total_sales
);
if ( $shared_capacity && $global_stock instanceof \Tribe__Tickets__Global_Stock ) {
$this->update_global_stock( $global_stock, $quantity );
}
return $updated_total_sales;
}
/**
* Decrease the sales for a ticket by a specific quantity.
*
* @todo TribeCommerceLegacy: This should be moved into using a Flag Action.
*
* @since 5.1.9
*
* @param int $ticket_id The ticket post ID.
* @param int $quantity The quantity to increase the ticket sales by.
* @param bool $shared_capacity Whether the ticket is using shared capacity.
* @param \Tribe__Tickets__Global_Stock|null $global_stock The stock object or null.
*
* @return int The new sales amount.
*/
public function decrease_ticket_sales_by( $ticket_id, $quantity = 1, $shared_capacity = false, $global_stock = null ) {
// Adjust sales.
$sales = (int) get_post_meta( $ticket_id, static::$sales_meta_key, true ) - $quantity;
// Prevent negatives.
$sales = max( $sales, 0 );
update_post_meta( $ticket_id, static::$sales_meta_key, $sales );
if ( $shared_capacity && $global_stock instanceof \Tribe__Tickets__Global_Stock ) {
$this->update_global_stock( $global_stock, $quantity, true );
}
return $sales;
}
/**
* Gets the product price value object
*
* @since 5.1.9
* @since 5.2.3 method signature changed to return an instance of Value instead of a string.
* @since 5.13.0 added new param to force regular price value return.
*
* @param int|WP_Post|null $product The ticket post ID or object.
* @param bool $force_regular Whether to force the regular price.
*
* @return \TEC\Tickets\Commerce\Utils\Value|null;
* @version 5.2.3
*
*/
public function get_price_value( $product, $force_regular = false ) {
$ticket = Models\Ticket_Model::from_post( $product );
if ( ! $ticket instanceof Models\Ticket_Model ) {
return;
}
$ticket_object = $this->get_ticket( $product );
if ( $force_regular ) {
return $ticket->get_price_value();
}
return $ticket_object->on_sale ? $ticket->get_sale_price_value() : $ticket->get_price_value();
}
/**
* Returns the ticket price html template
*
* @since 5.1.9
* @since 5.9.0 Updated sale price template arguments.
*
* @param int|object $product The ticket post ID or object.
* @param array|boolean $attendee The attendee data.
*
* @return string
*/
public function get_price_html( $product, $attendee = false ) {
$value = $this->get_price_value( $product );
$ticket = $this->get_ticket( $product );
return $this->get_template()->template(
'price',
[
'on_sale' => $ticket->on_sale,
'price' => $value,
'regular_price' => Value::create( $this->get_regular_price( $product ) ),
],
false
);
}
/**
* Get the event ID for the ticket.
*
* @since 5.5.0
*
* @param int $ticket_id The ticket post ID.
*
* @return mixed
*/
public function get_related_event_id( $ticket_id ) {
return get_post_meta( $ticket_id, static::$event_relation_meta_key, true );
}
/**
* Update attendee data for moved attendees.
*
* @since 5.6.7 removed the use of `$this->decrease_ticket_sales_by` as the move method already takes care of stock.
* @since 5.5.9
*
* @param int $ticket_id The ticket which has been moved.
* @param int $src_ticket_type_id The ticket type it belonged to originally.
* @param int $tgt_ticket_type_id The ticket type it now belongs to.
* @param int $src_event_id The event/post which the ticket originally belonged to.
* @param int $tgt_event_id The event/post which the ticket now belongs to.
* @param int $instigator_id The user who initiated the change.
*
* @return void
*/
public function handle_moved_ticket_updates( $attendee_id, $src_ticket_type_id, $tgt_ticket_type_id, $src_event_id, $tgt_event_id, $instigator_id ) {
$attendee = tec_tc_attendees()->where( 'ID', $attendee_id );
try {
$attendee->set( 'ticket_id', $tgt_ticket_type_id );
$attendee->set( 'event_id', $tgt_event_id );
} catch ( \Exception $e ) {
do_action( 'tribe_log', 'error', __CLASS__, [ 'message' => $e->getMessage() ] );
}
$attendee_data = $attendee->save();
// Update the ticket stock data.
$this->increase_ticket_stock_by( $src_ticket_type_id, 1 );
$this->decrease_ticket_stock_by( $tgt_ticket_type_id, 1 );
// Sync shared capacity.
/** @var \Tribe__Tickets__Tickets_Handler $handler */
$handler = tribe( 'tickets.handler' );
$handler->sync_shared_capacity( $src_event_id, tribe_tickets_get_capacity( $src_event_id ) );
}
/**
* Increase the ticket stock.
*
* @since 5.5.10
*
* @param int|WP_Post|null $ticket_id The ticket post ID.
* @param int $quantity The quantity to increase the ticket stock by.
*
* @return bool|int
*/
public function increase_ticket_stock_by( $ticket_id, $quantity = 1 ) {
$stock = (int) get_post_meta( $ticket_id, static::$stock_meta_key, true ) + $quantity;
return update_post_meta( $ticket_id, static::$stock_meta_key, $stock );
}
/**
* Decrease the ticket stock.
*
* @since 5.8.3
*
* @param int $ticket_id int The ticket post ID.
* @param int $quantity int The quantity to decrease the ticket stock by.
*
* @return bool|int
*/
public function decrease_ticket_stock_by( int $ticket_id, int $quantity = 1 ) {
$stock = (int) get_post_meta( $ticket_id, static::$stock_meta_key, true ) - $quantity;
$stock = max( 0, $stock );
return update_post_meta( $ticket_id, static::$stock_meta_key, $stock );
}
/**
* Process the sale price data.
*
* @since 5.9.0
*
* @param Ticket_Object $ticket The ticket post object.
* @param array<string,mixed> $raw_data The raw data from the request.
*
* @return void
*/
public function process_sale_price_data( Ticket_Object $ticket, array $raw_data ): void {
$sale_price_enabled = tribe_is_truthy( Arr::get( $raw_data, 'ticket_add_sale_price', false ) );
update_post_meta( $ticket->ID, static::$sale_price_checked_key, $sale_price_enabled );
if ( ! $sale_price_enabled ) {
$this->remove_sale_price_data( $ticket );
return;
}
$sale_price = Arr::get( $raw_data, 'ticket_sale_price', false );
$regular_price = Arr::get( $raw_data, 'ticket_price', false );
if ( $sale_price >= $regular_price ) {
$this->remove_sale_price_data( $ticket );
return;
}
update_post_meta( $ticket->ID, static::$sale_price_key, Value::create( $sale_price ) );
$this->process_sale_price_dates( $ticket, $raw_data );
}
/**
* Process the sale price dates.
*
* @since 5.9.0
*
* @param Ticket_Object $ticket The ticket post object.
* @param array<string,mixed> $raw_data The raw data from the request.
*
* @return void
*/
public function process_sale_price_dates( Ticket_Object $ticket, array $raw_data ): void {
if ( isset( $raw_data['ticket_sale_start_date'] ) ) {
$start_date = Date_Utils::maybe_format_from_datepicker( $raw_data['ticket_sale_start_date'] );
$start_date = empty( $start_date ) ? '' : gmdate( Date_Utils::DBDATEFORMAT, strtotime( $start_date ) );
update_post_meta( $ticket->ID, static::$sale_price_start_date_key, $start_date );
}
if ( isset( $raw_data['ticket_sale_end_date'] ) ) {
$end_date = Date_Utils::maybe_format_from_datepicker( $raw_data['ticket_sale_end_date'] );
$end_date = empty( $end_date ) ? '' : gmdate( Date_Utils::DBDATEFORMAT, strtotime( $end_date ) );
update_post_meta( $ticket->ID, static::$sale_price_end_date_key, $end_date );
}
}
/**
* Remove the sale price data.
*
* @since 5.9.0
*
* @param Ticket_Object $ticket The ticket post object.
*
* @return void
*/
public function remove_sale_price_data( Ticket_Object $ticket ): void {
$keys = [
static::$sale_price_checked_key,
static::$sale_price_key,
static::$sale_price_start_date_key,
static::$sale_price_end_date_key,
];
foreach ( $keys as $key ) {
delete_post_meta( $ticket->ID, $key );
}
}
/**
* Check if a ticket is on sale.
*
* @since 5.9.0
*
* @param Ticket_Object $ticket The ticket object.
*
* @return bool Whether the ticket is on sale.
*/
public function is_on_sale( Ticket_Object $ticket ): bool {
$sale_checked = get_post_meta( $ticket->ID, static::$sale_price_checked_key, true );
if ( ! $sale_checked ) {
return false;
}
$sale_price = get_post_meta( $ticket->ID, static::$sale_price_key, true );
if ( empty( $sale_price ) ) {
return false;
}
$start_date = get_post_meta( $ticket->ID, static::$sale_price_start_date_key, true );
$end_date = get_post_meta( $ticket->ID, static::$sale_price_end_date_key, true );
if ( empty( $start_date ) && empty( $end_date ) ) {
return true;
}
// Using the ticket's date helper to ensure the timezone for events are handled correctly.
$start = $ticket->get_date( $start_date );
$end = $ticket->get_date( $end_date );
$now = $ticket->get_date( 'today' );
// If the sale has no end date and the start date is in the past, the sale is on.
if ( $start <= $now && empty( $end ) ) {
return true;
}
return $now >= $start && $now <= $end;
}
/**
* Get the price of a ticket.
*
* @since 5.9.0
*
* @param Ticket_Object $ticket The ticket object.
*
* @return string The price of the ticket.
*/
public function get_price( Ticket_Object $ticket ): string {
if ( $ticket->on_sale ) {
return $this->get_sale_price( $ticket->ID );
}
return $this->get_regular_price( $ticket->ID );
}
/**
* Get the sale price of a ticket.
*
* @since 5.9.0
*
* @param int $ticket_id The ticket post ID.
*
* @return string The sale price of the ticket.
*/
public function get_sale_price( int $ticket_id ): string {
$sale_price = get_post_meta( $ticket_id, static::$sale_price_key, true );
if ( ! $sale_price ) {
return '';
}
if ( $sale_price instanceof Value ) {
return $sale_price->get_string();
}
return ( new Value( $sale_price ) )->get_string();
}
/**
* Get the regular price of a ticket.
*
* @since 5.9.0
*
* @param int $ticket_id The ticket post ID.
*
* @return string The regular price of the ticket.
*/
public function get_regular_price( int $ticket_id ): string {
return get_post_meta( $ticket_id, '_price', true );
}
/**
* Get the sale price details for a ticket.
*
* @since 5.9.0
*
* @param int $ticket_id The ticket post ID.
*
* @return array<string,string> The sale price details.
*/
public function get_sale_price_details( int $ticket_id ): array {
return [
'enabled' => get_post_meta( $ticket_id, static::$sale_price_checked_key, true ),
'sale_price' => $this->get_sale_price( $ticket_id ),
'start_date' => get_post_meta( $ticket_id, static::$sale_price_start_date_key, true ),
'end_date' => get_post_meta( $ticket_id, static::$sale_price_end_date_key, true ),
];
}
/**
* Get the ticket types.
*
* @since 5.24.0
*
* @return array The ticket types.
*/
protected function get_ticket_types(): array {
return [ self::POSTTYPE ];
}
/**
* Load the ticket object.
*
* @since 5.24.0
*
* @param int $ticket_id The ticket post ID.
*
* @return Ticket_Object|null The ticket object.
*/
public function load_ticket_object( int $ticket_id ): ?Ticket_Object {
return tribe( Module::class )->get_ticket( 0, $ticket_id );
}
}
|