<?php
namespace TEC\Tickets\Commerce;
use WP_Post;
use WP_Error;
use TEC\Tickets\Commerce;
use Tribe__Utils__Array as Arr;
use TEC\Tickets\Commerce\Communication\Email as Email_Communication;
use TEC\Tickets\Commerce\Reports\Attendees as Attendees_Reports;
/**
* Class Tickets Provider class for Tickets Commerce
*
* @since 5.1.9
*
* @package TEC\Tickets\Commerce
*/
class Module extends \Tribe__Tickets__Tickets {
/**
* Constructor.
*
* @since 5.1.9
* @since 5.23.0 Moved to do_init() to avoid issues with translations.
*/
public function __construct() {
// This needs to happen before parent construct.
$this->plugin_name = 'Tickets Commerce'; // Intentionally not translated.
parent::__construct();
$this->attendee_object = Attendee::POSTTYPE;
$this->attendee_ticket_sent = '_tribe_tpp_attendee_ticket_sent';
$this->attendee_optout_key = Attendee::$optout_meta_key;
$this->ticket_object = Ticket::POSTTYPE;
$this->event_key = Attendee::$event_relation_meta_key;
$this->attendee_event_key = Attendee::$event_relation_meta_key;
$this->checkin_key = Attendee::$checked_in_meta_key;
$this->order_key = Attendee::$order_relation_meta_key;
$this->refund_order_key = '_tribe_tpp_refund_order';
$this->security_code = Attendee::$security_code_meta_key;
$this->full_name = '_tribe_tpp_full_name';
$this->email = Attendee::$purchaser_email_meta_key;
}
/**
* {@inheritdoc}
*/
public $orm_provider = \TEC\Tickets\Commerce::PROVIDER;
/**
* Name of the CPT that holds Attendees (tickets holders).
*
* @since 5.1.9
*
* @var string
*/
const ATTENDEE_OBJECT = Attendee::POSTTYPE;
/**
* Name of the CPT that holds Orders
*
* @since 5.1.9
*
* @var string
*/
const ORDER_OBJECT = Order::POSTTYPE;
/**
* Meta key that relates Attendees and Events.
*
* @since 5.1.9
*
* @var string
*/
const ATTENDEE_EVENT_KEY = '_tec_tickets_commerce_event';
/**
* Meta key that relates Attendees and Products.
*
* @since 5.1.9
*
* @var string
*/
const ATTENDEE_PRODUCT_KEY = '_tec_tickets_commerce_ticket';
/**
* Meta key that relates Attendees and Orders.
*
* @since 5.1.9
*
* @var string
*/
const ATTENDEE_ORDER_KEY = '_tec_tickets_commerce_order';
/**
* Indicates if a ticket for this attendee was sent out via email.
*
* @since 5.1.9
*
* @var string
*/
public $attendee_ticket_sent;
/**
* Meta key that if this attendee wants to show on the attendee list
*
* @since 5.1.9
*
* @var string
*/
public $attendee_optout_key;
/**
* Meta key that if this attendee PayPal status
*
* @since 5.1.9
*
* @var string
*/
public $attendee_tpp_key;
/**
* Name of the CPT that holds Tickets
*
* @since 5.1.9
*
* @var string
*/
public $ticket_object;
/**
* Meta key that relates Products and Events
*
* @since 5.1.9
*
* @var string
*/
public $event_key;
/**
* Meta key that stores if an attendee has checked in to an event
*
* @since 5.1.9
*
* @var string
*/
public $checkin_key;
/**
* Meta key that ties attendees together by order
*
* @since 5.1.9
*
* @var string
*/
public $order_key;
/**
* Meta key that ties attendees together by refunded order
*
* @since 5.1.9
*
* @var string
*/
public $refund_order_key;
/**
* Meta key that holds the security code that's printed in the tickets
*
* @since 5.1.9
*
* @var string
*/
public $security_code;
/**
* Meta key that holds the full name of the tickets PayPal "buyer"
*
* @since 5.1.9
*
* @var string
*/
public $full_name;
/**
* Meta key that holds the email of the tickets PayPal "buyer"
*
* @since 5.1.9
*
* @var string
*/
public $email;
/**
* Meta key that holds the name of a ticket to be used in reports if the Product is deleted
*
* @since 5.1.9
*
* @var string
*/
public $deleted_product = '_tribe_deleted_product_name';
/**
* A variable holder if PayPal is loaded
*
* @since 5.1.9
*
* @var boolean
*/
protected $is_loaded = false;
/**
* This method is required for the module to properly load.
*
* @since 5.1.9
* @return static
*/
public static function get_instance() {
return tribe( static::class );
}
/**
* Hooks the module, happens on the `wp` hook.
*
* @since 5.22.0
*
* @return void
*/
public function hook() {
parent::hook();
// Overwrite with the translated name.
$this->plugin_name = __( 'Tickets Commerce', 'event-tickets' );
}
/**
* Registers all actions/filters
*
* @since 5.1.9
*/
public function hooks() {
// if the hooks have already been bound, don't do it again
if ( $this->is_loaded ) {
return false;
}
}
/**
* Send tickets email for attendees.
*
* @since 5.1.9
*
* @param array $attendees List of attendees.
* @param array $args {
* The list of arguments to use for sending ticket emails.
*
* @type string $subject The email subject.
* @type string $content The email content.
* @type string $from_name The name to send tickets from.
* @type string $from_email The email to send tickets from.
* @type array|string $headers The list of headers to send.
* @type array $attachments The list of attachments to send.
* @type string $provider The provider slug (rsvp, tpp, woo, edd).
* @type int $post_id The post/event ID to send the emails for.
* @type string|int $order_id The order ID to send the emails for.
* }
*
* @return int The number of emails sent successfully.
*/
public function send_tickets_email_for_attendees( $attendees, $args = [] ) {
$args = array_merge(
[
'subject' => tribe_get_option( Settings::$option_confirmation_email_subject, false ),
'from_name' => tribe_get_option( Settings::$option_confirmation_email_sender_name, false ),
'from_email' => tribe_get_option( Settings::$option_confirmation_email_sender_email, false ),
'provider' => Commerce::ABBR,
],
$args
);
return parent::send_tickets_email_for_attendees( $attendees, $args );
}
/**
* Shows the tickets form in the front end
*
* @since 5.1.9
*
* @param string $unused_content Unused content.
*
* @return void
*/
public function front_end_tickets_form( $unused_content ) {
$post = $GLOBALS['post'];
$tickets = $this->get_tickets( $post->ID );
foreach ( $tickets as $index => $ticket ) {
if ( __CLASS__ !== $ticket->provider_class ) {
unset( $tickets[ $index ] );
}
}
if ( empty( $tickets ) ) {
return;
}
tribe( Tickets_View::class )->get_tickets_block( $post->ID );
}
/**
* Indicates if we currently require users to be logged in before they can obtain
* tickets.
*
* @since 5.1.9
*
* @return bool
*/
public function login_required() {
$requirements = (array) tribe_get_option( 'ticket-authentication-requirements', array() );
return in_array( 'event-tickets_all', $requirements, true );
}
/**
* Get attendees by id and associated post type
* or default to using $post_id
*
* @since 5.1.9
*
* @param int|string $post_id The post ID.
* @param string|null $post_type The post type.
*
* @return array|mixed
*/
public function get_attendees_by_id( $post_id, $post_type = null ) {
if ( ! $post_type ) {
$post_type = get_post_type( $post_id );
}
switch ( $post_type ) {
case $this->attendee_object:
return $this->get_attendees_by_attendee_id( $post_id );
case 'tpp_order_hash':
return $this->get_attendees_by_order_id( $post_id );
case $this->ticket_object:
return tribe( Attendee::class )->get_attendees_by_ticket_id( $post_id, $this->orm_provider );
default:
return $this->get_attendees_by_post_id( $post_id );
}
}
/**
* Get attendees for a ticket by order ID, optionally by ticket ID.
*
* This overrides the parent method because Tickets Commerce stores the order ID in the post_parent.
*
* @since 5.2.0
*
* @param int|string $order_id Order ID.
* @param null|int $ticket_id (optional) Ticket ID.
*
* @return array List of attendees.
*/
public function get_attendees_by_order_id( $order_id ) {
$ticket_id = null;
// Support an optional second argument while not causing warnings from other ticket provider classes.
if ( 1 < func_num_args() ) {
$ticket_id = func_get_arg( 1 );
}
/** @var \Tribe__Tickets__Attendee_Repository $repository */
$repository = tribe_attendees( $this->orm_provider );
$repository->by( 'parent', $order_id );
if ( $ticket_id ) {
$repository->by( 'ticket', $ticket_id );
}
return $this->get_attendees_from_module( $repository->all() );
}
/**
* Returns the value of a key defined by the class.
*
* @since 5.1.9
*
* @param string $key The key.
*
* @return string The key value or an empty string if not defined.
*/
public static function get_key( $key ) {
$instance = self::get_instance();
$key = strtolower( $key );
$constant_map = [
'attendee_event_key' => $instance->attendee_event_key,
'attendee_product_key' => $instance->attendee_product_key,
'attendee_order_key' => $instance->order_key,
'attendee_optout_key' => $instance->attendee_optout_key,
'event_key' => $instance->get_event_key(),
'checkin_key' => $instance->checkin_key,
'order_key' => $instance->order_key,
];
return \Tribe__Utils__Array::get( $constant_map, $key, '' );
}
/**
* Indicates if global stock support is enabled for this provider.
*
* @since 5.1.9
*
* @return bool
*/
public function supports_global_stock() {
/**
* Allows the declaration of global stock support for Tribe Commerce tickets
* to be overridden.
*
* @param bool $enable_global_stock_support
*/
return (bool) apply_filters( 'tec_tickets_commerce_enable_global_stock', true );
}
/**
* All the methods below here were created merely as a backwards compatibility piece for our old Code that
* depends so much on the concept of a Main class handling all kinds of integration pieces.
*
* ! DO NOT INTRODUCE MORE LOGIC OR COMPLEXITY ON THESE METHODS !
*
* The methods are all focused on routing functionality to their correct handlers.
*/
/**
* Returns the ticket price html template.
*
* @since 5.1.9
*
* @param int|object $product The product.
* @param array|boolean $attendee The attendee.
*
* @return string
*/
public function get_price_html( $product, $attendee = false ) {
return tribe( Ticket::class )->get_price_html( $product );
}
/**
* Gets the product price value
*
* @since 5.1.9
*
* @param int|WP_post $product The product.
*
* @return string
*/
public function get_price_value( $product ) {
return tribe( Ticket::class )->get_price_value( $product );
}
/**
* Whether a specific attendee is valid toward inventory decrease or not.
*
* By default only attendees generated as part of a Completed order will count toward
* an inventory decrease but, if the option to reserve stock for Pending Orders is activated,
* then those attendees generated as part of a Pending Order will, for a limited time after the
* order creation, cause the inventory to be decreased.
*
* @since 5.1.9
*
* @param array $attendee The attendee.
*
* @return bool
*/
public function attendee_decreases_inventory( array $attendee ) {
return tribe( Attendee::class )->decreases_inventory( $attendee );
}
/**
* {@inheritdoc}
*/
public function get_attendee( $attendee, $post_id = 0 ) {
return tec_tc_get_attendee( $attendee, ARRAY_A );
}
/**
* Event Tickets Plus Admin Reports page will use this data from this method.
*
* @since 5.1.9
*
*
* @param string|int $order_id
*
* @return array
*/
public function get_order_data( $order_id ) {
return tec_tc_get_order( $order_id, ARRAY_A );
}
/**
* Renders the advanced fields in the new/edit ticket form.
* Using the method, providers can add as many fields as
* they want, specific to their implementation.
*
* @since 5.1.9
*
* @param int $post_id The post ID.
* @param int $ticket_id The ticket ID.
*/
public function do_metabox_capacity_options( $post_id, $ticket_id ) {
tribe( Editor\Metabox::class )->do_metabox_capacity_options( $post_id, $ticket_id );
}
/**
* Maps to the Cart Class method to get the cart.
*
* @since 5.1.9
*
* @return string The cart URL.
*/
public function get_cart_url() {
return tribe( Cart::class )->get_url();
}
/**
* Maps to the Checkout Class method to get the checkout.
*
* @since 5.2.0
*
* @return string The checkout URL.
*/
public function get_checkout_url() {
return tribe( Checkout::class )->get_url();
}
/**
* Generate and store all the attendees information for a new order.
*
* @since 5.1.9
* @deprecated 5.2.0
*
* @param string $payment_status The tickets payment status, defaults to completed.
* @param bool $redirect Whether the client should be redirected or not.
*/
public function generate_tickets( $payment_status = 'completed', $redirect = true ) {
_deprecated_function( __METHOD__, '5.2.0' );
}
/**
* Gets an individual ticket.
*
* @since 5.1.9
* @since 5.6.7 Set some provider-invariant ticket properties.
*
* @param int|WP_post $post_id
* @param int|WP_post $ticket_id
*
* @return null|\Tribe__Tickets__Ticket_Object
*/
public function get_ticket( $post_id, $ticket_id ) {
$ticket = tribe( Ticket::class )->get_ticket( $ticket_id );
if ( ! $ticket instanceof \Tribe__Tickets__Ticket_Object ) {
return null;
}
// Set provider-invariant ticket properties.
$ticket_type = get_post_meta( $ticket_id, '_type', true ) ?: 'default';
$ticket->type = $ticket_type;
return $ticket;
}
/**
* Saves a Tickets Commerce ticket.
*
* @since 5.1.9
*
* @param int $post_id Post ID.
* @param \Tribe__Tickets__Ticket_Object $ticket Ticket object.
* @param array $raw_data Ticket data.
*
* @return int|false The updated/created ticket post ID or false if no ticket ID.
*/
public function save_ticket( $post_id, $ticket, $raw_data = [] ) {
// Run anything we might need on parent method.
parent::save_ticket( $post_id, $ticket, $raw_data );
/**
* Important, do not add anything above this method.
* Our goal is to reduce the amount of load on the `Module`, relegate these behaviors to the correct models.
*/
return tribe( Ticket::class )->save( $post_id, $ticket, $raw_data );
}
/**
* Deletes a ticket or an attendee post.
*
* @since 5.1.9
*
* @since 5.5.10 Adjust the method to handle both Ticket and Attendee post type deletion separately.
*
* @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_ticket( $event_id, $ticket_id ) {
/**
* Important, do not add anything above this method.
* Our goal is to reduce the amount of load on the `Module`, relegate these behaviors to the correct models.
*/
$ticket_post = get_post( $ticket_id );
if ( ! $ticket_post instanceof WP_Post ) {
return false;
}
$deleted = false;
// We are handling both Ticket and Attendee post type deletion using this same method.
if ( Attendee::POSTTYPE === $ticket_post->post_type ) {
$deleted = tribe( Attendee::class )->delete( $ticket_id );
}
if ( Ticket::POSTTYPE === $ticket_post->post_type ) {
$deleted = tribe( Ticket::class )->delete( $event_id, $ticket_id );
}
if ( ! $deleted ) {
return false;
}
// Run anything we might need on parent method.
parent::delete_ticket( $event_id, $ticket_id );
return true;
}
/**
* Return whether we're currently on the checkout page for Tickets Commerce.
*
* @since 5.1.9
*
* @return bool
*/
public function is_checkout_page() {
return tribe( Checkout::class )->is_current_page();
}
/**
* Links to sales report for all tickets for this event.
*
* @since 5.1.9
*
* @param int $event_id The event ID.
* @param bool $url_only Whether to return the URL only.
*
* @return string The event reports link.
*/
public function get_event_reports_link( $event_id, $url_only = false ) {
return tribe( Commerce\Reports\Orders::class )->get_event_link( $event_id, $url_only );
}
/**
* Links to the sales report for this product.
*
* @since 5.1.9
*
* @param int|WP_Post|null $event_id The event ID.
* @param int|WP_Post|null $ticket_id The ticket ID.
*
* @return string The ticket reports link.
*/
public function get_ticket_reports_link( $event_id, $ticket_id ) {
return tribe( Commerce\Reports\Orders::class )->get_ticket_link( $event_id, $ticket_id );
}
/**
* Create an attendee for the Commerce provider from a ticket.
*
* @since 5.1.0
*
* @param \Tribe__Tickets__Ticket_Object|int $ticket Ticket object or ID to create the attendee for.
* @param array $attendee_data Attendee data to create from.
*
* @return WP_post|WP_Error|false The new post object or false if we can't resolve to a Ticket object. WP_Error if modifying status fails.
*/
public function create_attendee( $ticket, $attendee_data ) {
// Get the ticket object from the ID.
if ( is_numeric( $ticket ) ) {
$ticket = $this->get_ticket( 0, (int) $ticket );
}
// If the ticket is not valid, stop creating the attendee.
if ( ! $ticket instanceof \Tribe__Tickets__Ticket_Object ) {
return false;
}
$extra = [];
$extra['attendees'] = [
1 => [
'meta' => Arr::get( $attendee_data, 'attendee_meta', [] )
]
];
$extra['optout'] = ! Arr::get( $attendee_data, 'send_ticket_email', true );
$extra['iac'] = false;
// The Manual Order takes the same format as the cart items.
$items = [
$ticket->ID => [
'ticket_id' => $ticket->ID,
'quantity' => 1,
'extra' => $extra,
]
];
$purchaser = [
'full_name' => $attendee_data['full_name'],
'email' => $attendee_data['email'],
// By default user ID is zero here.
'user_id' => 0,
];
$order = tribe( Gateways\Manual\Order::class )->create( $items, $purchaser );
/**
* For now we need to make sure we move to pending before completed.
*
* @todo @backend when an order is moved into completed they need to update to pending first.
* likely we should have this be developed by implementing a dependent status.
*/
$updated = tribe( Order::class )->modify_status( $order->ID, 'pending' );
if ( is_wp_error( $updated ) ) {
return $updated;
}
$updated = tribe( Order::class )->modify_status( $order->ID, 'completed' );
if ( is_wp_error( $updated ) ) {
return $updated;
}
$attendee = tec_tc_attendees()->by( 'order_id', $order->ID )->first();
return $attendee;
}
/**
* Update an attendee for the Commerce provider.
*
* @since 5.2.0
*
* @todo TribeLegacyCommerce We need to move this into the Attendee class.
*
* @param array|int $attendee The attendee data or ID for the attendee to update.
* @param array $attendee_data The attendee data to update to.
*
* @return WP_post|false The updated post object or false if unsuccessful.
*/
public function update_attendee( $attendee, $attendee_data ) {
if ( is_numeric( $attendee ) ) {
$attendee_id = (int) $attendee;
} elseif ( is_array( $attendee ) && isset( $attendee['attendee_id'] ) ) {
$attendee_id = (int) $attendee['attendee_id'];
} else {
return false;
}
$attendee = tec_tc_attendees( $this->orm_provider )
->where( 'ID', $attendee_id );
try {
if ( ! empty( $attendee_data['attendee_meta'] ) ) {
$attendee->set( 'fields', $attendee_data['attendee_meta'] );
}
if ( ! empty( $attendee_data['full_name'] ) ) {
$attendee->set( 'full_name', $attendee_data['full_name'] );
}
if ( ! empty( $attendee_data['email'] ) && filter_var( $attendee_data['email'], FILTER_VALIDATE_EMAIL ) ) {
$attendee->set( 'email', $attendee_data['email'] );
}
if ( isset( $attendee_data['check_in'] ) ) {
$attendee->set( 'checked_in', $attendee_data['check_in'] );
if ( $attendee_data['check_in'] ) {
parent::checkin( $attendee_id );
} else {
parent::uncheckin( $attendee_id );
}
}
$attendee->save();
// Send attendee email.
$send_ticket_email = (bool) Arr::get( $attendee_data, 'send_ticket_email', false );
$send_ticket_email_args = (array) Arr::get( $attendee_data, 'send_ticket_email_args', [] );
// Check if we need to send the ticket email.
if ( $send_ticket_email ) {
$attendee_tickets = [
$attendee_id,
];
// Maybe send the attendee email.
$this->send_tickets_email_for_attendees( $attendee_tickets, $send_ticket_email_args );
}
} catch ( \Tribe__Repository__Usage_Error $e ) {
do_action( 'tribe_log', 'error', __CLASS__, [ 'message' => $e->getMessage() ] );
return false;
}
return $attendee;
}
/**
* Update the email sent counter for attendee by increasing it +1.
*
* @since 5.8.0
*
* @param int $attendee_id The attendee ID.
*/
public function update_ticket_sent_counter( $attendee_id ) {
tribe( Email_Communication::class )->update_ticket_sent_counter( $attendee_id );
}
}
|