<?php
/**
* Cart handling.
*
* @category Core
* @package My Tickets
* @author Joe Dolson
* @license GPLv2 or later
* @link https://www.joedolson.com/my-tickets/
*/
add_action( 'init', 'mt_handle_cart' );
/**
* Handle cart submission. Receive data, create payment, delete cart if applicable, register message.
*/
function mt_handle_cart() {
$options = mt_get_settings();
if ( ! isset( $_POST['mt_submit'] ) ) {
return;
} else {
$nonce = $_POST['_wpnonce'];
if ( ! wp_verify_nonce( $nonce, 'mt_cart_nonce' ) ) {
die;
}
// add filter here to handle required custom fields in cart TODO.
$email_valid = ( isset( $_POST['mt_email'] ) ) ? is_email( $_POST['mt_email'] ) : false;
if ( ! $email_valid || ! isset( $_POST['mt_fname'] ) || '' === $_POST['mt_fname'] || ! isset( $_POST['mt_lname'] ) || '' === $_POST['mt_lname'] || ! isset( $_POST['mt_email'] ) || '' === $_POST['mt_email'] || ! isset( $_POST['mt_email2'] ) || $_POST['mt_email'] !== $_POST['mt_email2'] ) {
$url = add_query_arg( 'response_code', 'required-fields', get_permalink( $options['mt_purchase_page'] ) );
wp_safe_redirect( $url );
exit;
}
$post = map_deep( $_POST, 'sanitize_text_field' );
$payment = mt_create_payment( $post );
if ( $payment ) {
// Handle custom fields added to cart form.
do_action( 'mt_handle_custom_cart_data', $payment, $post );
if ( ! ( mt_get_data( 'payment' ) ) ) {
mt_debug( print_r( $post, 1 ), 'Data passed from client at payment creation', $payment );
mt_save_data( $payment, 'payment' );
}
} else {
mt_register_message( 'payment', 'error' );
}
}
}
/**
* Verify payment status. If a payment is completed, do not re-use that payment record.
*
* @param integer $payment Payment ID.
*
* @return boolean
*/
function mt_is_payment_completed( $payment ) {
$payment_status = get_post_meta( $payment, '_is_paid', true );
if ( 'Completed' === $payment_status ) {
return true;
}
return false;
}
/**
* Generates new payment from POSTed data.
*
* @param array $post POST data.
*
* @return array|int|mixed|WP_Error
*/
function mt_create_payment( $post ) {
$options = mt_get_settings();
// save payment post.
$current_user = wp_get_current_user();
if ( isset( $post['mt_purchaser_id'] ) ) {
$purchaser = absint( $post['mt_purchaser_id'] );
} else {
$purchaser = ( is_user_logged_in() ) ? $current_user->ID : 0;
}
$date = ( isset( $post['mt_purchase_date'] ) ) ? $post['mt_purchase_date'] : mt_date( 'Y-m-d H:i:00', mt_current_time(), false );
$payment = mt_get_data( 'payment' );
if ( ! is_string( get_post_status( $payment ) ) || 'trash' === get_post_status( $payment ) ) {
$payment = false;
}
if ( $payment && ! mt_is_payment_completed( $payment ) ) {
$purchase_id = mt_get_data( 'payment' );
$status = 'draft';
} else {
mt_delete_data( 'payment' );
$status = 'draft';
$date = $date;
$post_title = sanitize_text_field( $post['mt_fname'] . ' ' . $post['mt_lname'] );
$my_post = array(
'post_title' => $post_title,
'post_content' => wp_json_encode( $post ),
'post_status' => 'draft',
'post_author' => $purchaser,
'post_date' => $date,
'post_type' => 'mt-payments',
);
$purchase_id = wp_insert_post( $my_post );
}
/**
* Action immediately after new payment post is created.
*
* @hook mt_after_insert_payment
*
* @param {int} $purchase_id Payment post ID.
* @param {array} $post Array of data passed to function.
*/
do_action( 'mt_after_insert_payment', $purchase_id, $post );
update_post_meta( $purchase_id, '_first_name', sanitize_text_field( $post['mt_fname'] ) );
update_post_meta( $purchase_id, '_last_name', sanitize_text_field( $post['mt_lname'] ) );
if ( isset( $options['mt_ticket_handling'] ) && is_numeric( $options['mt_ticket_handling'] ) ) {
update_post_meta( $purchase_id, '_ticket_handling', $options['mt_ticket_handling'] );
}
$email = $post['mt_email'];
update_post_meta( $purchase_id, '_email', sanitize_email( $email ) );
$phone = ( isset( $post['mt_phone'] ) ) ? $post['mt_phone'] : '';
update_post_meta( $purchase_id, '_phone', sanitize_text_field( $phone ) );
if ( $purchaser ) {
update_user_meta( $purchaser, 'mt_phone', sanitize_text_field( $phone ) );
}
$vat = ( isset( $post['mt_vat'] ) ) ? $post['mt_vat'] : '';
update_post_meta( $purchase_id, '_vat', sanitize_text_field( $vat ) );
if ( is_user_logged_in() ) {
update_user_meta( $purchaser, 'mt_vat', sanitize_text_field( $vat ) );
}
$purchased = ( isset( $post['mt_cart_order'] ) ) ? $post['mt_cart_order'] : false;
$paid = mt_calculate_cart_cost( $purchased, $purchase_id );
$gateway = sanitize_text_field( $post['mt_gateway'] );
if ( isset( $options['mt_handling'] ) && is_numeric( $options['mt_handling'] ) ) {
$handling_total = mt_get_cart_handling( $options, $gateway );
$paid = $paid + $handling_total;
update_post_meta( $purchase_id, '_mt_handling', $handling_total );
}
if ( isset( $options['mt_shipping'] ) && 'postal' === $post['ticketing_method'] ) {
$paid = $paid + $options['mt_shipping'];
update_post_meta( $purchase_id, '_mt_shipping', $options['mt_shipping'] );
}
update_post_meta( $purchase_id, '_total_paid', $paid );
$payment_status = ( 0 === (float) $paid ) ? 'Completed' : 'Pending';
update_post_meta( $purchase_id, '_is_paid', $payment_status );
if ( is_user_logged_in() && ! is_admin() && '' !== trim( $options['mt_members_discount'] ) ) {
update_post_meta( $purchase_id, '_discount', $options['mt_members_discount'] );
}
update_post_meta( $purchase_id, '_gateway', $gateway );
update_post_meta( $purchase_id, '_purchase_data', $purchased );
// Debugging.
mt_debug( print_r( $purchased, 1 ), 'Purchase Data saved at nav to payment screen', $purchase_id );
mt_debug( print_r( $post, 1 ), 'Data passed from client at nav to payment screen', $purchase_id );
update_post_meta( $purchase_id, '_ticketing_method', $post['ticketing_method'] );
/**
* Action run when payment is saved.
*
* @hook mt_save_payment_fields
*
* @param {int} $purchase_id Payment post ID.
* @param {array} $post Array of data passed to function.
* @param {array} $purchased Array of ticket purchase data from cart.
*/
do_action( 'mt_save_payment_fields', $purchase_id, $post, $purchased );
if ( $purchase_id ) {
wp_update_post(
array(
'ID' => $purchase_id,
'post_name' => 'mt_payment_' . $purchase_id,
)
);
}
$receipt_id = md5(
add_query_arg(
array(
'post_type' => 'mt-payments',
'p' => $purchase_id,
),
home_url()
)
);
update_post_meta( $purchase_id, '_receipt', $receipt_id );
if ( 'publish' === $status ) {
wp_update_post(
array(
'ID' => $purchase_id,
'post_status' => $status,
)
);
}
return $purchase_id;
}
/**
* Get inventory change comparing submitted cart data and existing cart data.
*
* @param array $passed Data passed from cart form. If empty, removing existing cart.
* @param array $saved Data currently saved in cart.
*
* @return array Array of changes to record.
*/
function mt_get_inventory_change( $passed = array(), $saved = array() ) {
$remove = false;
if ( empty( $saved ) ) {
$saved = mt_get_cart( false, false, true );
}
if ( empty( $passed ) ) {
// If no data passed, we need to remove the current cart.
$passed = $saved;
$remove = true;
}
$changes = array();
foreach ( $passed as $event_id => $counts ) {
if ( ! is_array( $counts ) ) {
continue;
}
foreach ( $counts as $type => $new_count ) {
$old_count = absint( ( isset( $saved[ $event_id ] ) ) ? $saved[ $event_id ][ $type ] : 0 );
$new_count = absint( $new_count );
// If no change, don't include unless removing cart.
if ( $new_count !== $old_count || $remove ) {
$increment = ( $remove ) ? ( $old_count * -1 ) : ( $old_count - $new_count ) * -1;
$changes[ $type ] = array(
'event_id' => $event_id,
'count' => $increment,
'old' => $old_count,
'new' => $new_count,
);
}
}
}
return $changes;
}
/**
* Update virtual inventory for an event.
*
* @param int $event_id Event post ID.
* @param string $type Type of ticket changing.
* @param int $count Number of tickets to add or substract.
*/
function mt_update_inventory( $event_id, $type, $count ) {
$virtual_inventory = get_post_meta( $event_id, '_mt_virtual_inventory', true );
if ( ! is_array( $virtual_inventory ) ) {
$virtual_inventory = array();
}
if ( isset( $virtual_inventory[ $type ] ) ) {
$current = $virtual_inventory[ $type ];
$new = ( ( $current + $count ) < 0 ) ? 0 : $current + $count;
$virtual_inventory[ $type ] = $new;
} else {
// Can't initialize store with negative values.
if ( $count > 0 ) {
$virtual_inventory[ $type ] = absint( $count );
}
}
update_post_meta( $event_id, '_mt_virtual_inventory', $virtual_inventory );
}
/**
* Determine how many total real tickets have been sold for a given pricing set. Ignores virtual inventory.
*
* @param array $pricing Pricing array.
* @param string|integer $available Available tickets.
*
* @return array|bool
*/
function mt_tickets_left( $pricing, $available ) {
$total = 0;
$sold = 0;
foreach ( $pricing as $options ) {
if ( 'inherit' !== $available ) {
$sold = $sold + intval( $options['sold'] );
} else {
$tickets = intval( $options['tickets'] ) - intval( $options['sold'] );
$total = $total + $tickets;
$sold = $sold + intval( $options['sold'] );
}
}
if ( 'inherit' !== $available && is_numeric( trim( $available ) ) ) {
$total = $available - $sold;
}
return array(
'remain' => $total,
'sold' => $sold,
'total' => $sold + $total,
);
}
/**
* Check virtual inventory for an event and ticket group. Returns total tickets if ticket type not passed.
*
* @param int $event_id Event post ID.
* @param string $type Type of ticket being checked.
* @param bool|string $virtual Whether to return the virtual inventory. 'auto' to return according to settings.
* True to return virtual. False to return true inventory.
*
* @return array|false Number of tickets available in key 'available', sold in key 'sold'.
*/
function mt_check_inventory( $event_id, $type = '', $virtual = 'auto' ) {
$options = mt_get_settings();
$registration = get_post_meta( $event_id, '_mt_registration_options', true );
// If sales are closed for this event or this event type, return real inventory instead of virtual.
if ( mt_event_expired( $event_id ) || $type && mt_ticket_type_expired( $event_id, $type ) ) {
$virtual = false;
}
if ( ! is_array( $registration ) ) {
return false;
}
$prices = $registration['prices'];
if ( ( 'discrete' === $registration['counting_method'] || 'event' === $registration['counting_method'] ) ) {
if ( '' !== $type ) {
$sold = absint( isset( $prices[ $type ]['sold'] ) ? $prices[ $type ]['sold'] : 0 );
$available = absint( $prices[ $type ]['tickets'] ) - $sold;
} else {
$available = 0;
$sold = 0;
foreach ( $prices as $pricetype ) {
$available += (int) $pricetype['tickets'];
$sold += (int) $pricetype['sold'];
}
$available = $available - $sold;
}
} else {
$available = absint( $registration['total'] );
$sold = 0;
foreach ( $prices as $pricetype ) {
$sold = $sold + intval( ( isset( $pricetype['sold'] ) ) ? $pricetype['sold'] : 0 );
}
$available = $available - $sold;
}
/**
* Filter whether a particular event uses virtual inventory. Return 'virtual' to use the virtual inventory, 'actual' to use completed purchases only.
*
* @hook mt_is_virtual_inventory
*
* @param {string} $mt_inventory 'actual' or 'virtual'.
* @param {int} $event_id Event ID.
*
* @return {string}
*/
$is_virtual = apply_filters( 'mt_is_virtual_inventory', $options['mt_inventory'], $event_id );
if ( 'virtual' === $is_virtual && ( 'auto' === $virtual || true === $virtual ) ) {
// Virtual inventory holds tickets in carts but not yet sold.
$virtual_inventory = get_post_meta( $event_id, '_mt_virtual_inventory', true );
/**
* Filter the virtual inventory array.
*
* @hook mt_virtual_inventory
*
* @param {array} $virtual_inventory Contents of the virtual inventory.
* @param {int} $event_id Current Event ID.
* @param {array} $registration Event registration configuration.
*
* @return {array}
*/
$virtual_inventory = apply_filters( 'mt_virtual_inventory', $virtual_inventory, $event_id, $registration );
if ( '' !== $type ) {
$current_virtual = isset( $virtual_inventory[ $type ] ) ? $virtual_inventory[ $type ] : 0;
} else {
$current_virtual = 0;
if ( is_array( $virtual_inventory ) ) {
foreach ( $virtual_inventory as $type => $quantity ) {
$current_virtual += (int) $quantity;
}
}
}
$available = $available - $current_virtual;
$sold = $sold + $current_virtual;
}
return array(
'available' => $available,
'sold' => $sold,
'total' => $available + $sold,
);
}
/**
* Generates tickets for purchase.
*
* @param integer $purchase_id Payment ID.
* @param bool|array $purchased Array when initially building tickets, false otherwise.
* @param bool $resending We're resending a notice right now.
*
* @return null
*/
function mt_create_tickets( $purchase_id, $purchased = false, $resending = false ) {
// _purchase_data contains the original purchase info; it's not updated when something is moved.
$purchased = ( $purchased ) ? $purchased : get_post_meta( $purchase_id, '_purchase_data', true );
if ( ! is_array( $purchased ) || mt_purchase_has_tickets( $purchase_id ) ) {
return;
}
$ids = array();
foreach ( $purchased as $event_id => $purchase ) {
// It's possible for an event ID to appear in this list twice. If so, ignore the repetitions; they're duplicates.
if ( in_array( $event_id, $ids, true ) ) {
continue;
}
$registration = get_post_meta( $event_id, '_mt_registration_options', true );
$created = false;
$ids[] = $event_id;
add_post_meta( $purchase_id, '_purchased', array( $event_id => $purchase ) );
add_post_meta( $event_id, '_purchase', array( $purchase_id => $purchase ) );
foreach ( $purchase as $type => $ticket ) {
// add ticket hash for each ticket.
$count = $ticket['count'];
$price = $ticket['price'];
$sold = absint( $registration['prices'][ $type ]['sold'] );
$new_sold = $sold + $count;
$registration['prices'][ $type ]['sold'] = $new_sold;
for ( $i = 0; $i < $count; $i++ ) {
$ticket_id = mt_generate_ticket_id( $purchase_id, $event_id, $type, $i, $price );
if ( ! $resending && ! mt_ticket_exists( $purchase_id, $ticket_id ) ) {
$created = true;
add_post_meta( $event_id, '_ticket', $ticket_id );
update_post_meta(
$event_id,
'_' . $ticket_id,
array(
'type' => $type,
'price' => $price,
'purchase_id' => $purchase_id,
)
);
}
}
if ( ! $resending && $created ) {
update_post_meta( $event_id, '_mt_registration_options', $registration );
}
}
}
}
/**
* Check whether this purchase has already had tickets created.
*
* @param int $purchase_id Payment ID.
*
* @return boolean
*/
function mt_purchase_has_tickets( $purchase_id ) {
// This crudely checks whether the _purchased data point is created, but doesn't check the entire list of tickets.
$tickets = get_post_meta( $purchase_id, '_purchased', true );
if ( is_array( $tickets ) && ! empty( $tickets ) ) {
return true;
}
return false;
}
/**
* Check whether this ticket ID already exists.
*
* @param int $purchase_id Payment ID.
* @param string $ticket_id Ticket ID string.
*
* @return boolean
*/
function mt_ticket_exists( $purchase_id, $ticket_id ) {
global $wpdb;
$value = $wpdb->get_var( $wpdb->prepare( "SELECT meta_key FROM $wpdb->postmeta WHERE post_id = %d AND meta_value = %s", $purchase_id, $ticket_id ) );
return ( $value ) ? true : false;
}
/**
* Check whether this ticket ID exists in the event. (Handles tickets that have been removed from purchase pool.)
*
* @param int $event_id Event ID.
* @param string $ticket_id Ticket ID string.
*
* @return boolean
*/
function mt_ticket_exists_on_event( $event_id, $ticket_id ) {
global $wpdb;
$value = $wpdb->get_var( $wpdb->prepare( "SELECT meta_key FROM $wpdb->postmeta WHERE post_id = %d AND meta_value = %s", $event_id, $ticket_id ) );
return ( $value ) ? true : false;
}
/**
* Generates the ticket ID from purchase ID, ticket type, number of ticket purchased of that time, and price.
*
* @param integer $purchase_id Payment ID.
* @param integer $event_id Event ID for this ticket.
* @param string $type Type of ticket purchased.
* @param integer $i Count; how many of this type have been purchased in this payment.
* @param float $price Price for this ticket.
*
* @return string
*/
function mt_generate_ticket_id( $purchase_id, $event_id, $type, $i, $price ) {
// hash data.
$hash = md5( $purchase_id . $type . $i . $price . $event_id );
// reduce to 13 chars.
$hash = substr( $hash, 0, 12 );
// seed with $type substring & ticket type ID.
$hash = substr( $type, 0, 2 ) . $hash . zeroise( $i, 4 );
$args = array(
'purchase_id' => $purchase_id,
'event_id' => $event_id,
'type' => $type,
'i' => $i,
'price' => $price,
);
mt_generate_sequential_id( $hash, $args );
return apply_filters( 'mt_generate_ticket_id', $hash, $args );
}
/**
* Generate a sequential ID for each ticket.
*
* @param string $hash Hash ID for ticket.
* @param array $args Array of ticket ID generation information.
*
* @return int sequential ID
*/
function mt_generate_sequential_id( $hash, $args ) {
$sequential_base = get_post_meta( $args['event_id'], '_sequential_base', true );
$sequential_id = ( $sequential_base ) ? $sequential_base + 1 : 1;
if ( ! get_post_meta( $args['event_id'], '_' . $hash . '_seq_id', true ) ) {
update_post_meta( $args['event_id'], '_' . $hash . '_seq_id', $sequential_id );
update_post_meta( $args['event_id'], '_sequential_base', $sequential_id );
}
return $sequential_id;
}
/**
* Calculates cost of cart. (Actual cost, after discounts.)
*
* @param array $purchased Tickets purchased.
* @param int $payment_id Payment ID. Use for calculating discounts.
*
* @return float
*/
function mt_calculate_cart_cost( $purchased, $payment_id ) {
$total = 0;
if ( $purchased ) {
foreach ( $purchased as $event_id => $tickets ) {
$prices = mt_get_prices( $event_id, $payment_id );
if ( $prices ) {
foreach ( $tickets as $type => $ticket ) {
if ( (int) $ticket['count'] > 0 ) {
$price = ( isset( $prices[ $type ] ) ) ? $prices[ $type ]['price'] : '';
if ( $price ) {
$price = mt_handling_price( $price, $event_id );
}
$total = $total + ( $price * $ticket['count'] );
}
}
}
}
}
$total = apply_filters( 'mt_apply_discounts', $total, $purchased, $payment_id );
return round( $total, 2 );
}
/**
* Compares price paid by customer to expected price of cart.
*
* @param float $price Total amount paid.
* @param int $purchase_id Payment ID to compare against.
*
* @return float Checked value
*/
function mt_check_payment_amount( $price, $purchase_id ) {
$total_paid = get_post_meta( $purchase_id, '_total_paid', true );
$donation = get_post_meta( $purchase_id, '_donation', true );
$total = (float) ( (float) $total_paid + (float) $donation );
return $total;
}
/**
* Execute a refresh of the My Tickets primary URL caches if caching plug-in installed.
*/
function mt_refresh_cache() {
$options = mt_get_settings();
$receipts = $options['mt_receipt_page'];
$tickets = $options['mt_tickets_page'];
$purchase = $options['mt_purchase_page'];
$to_refresh = apply_filters( 'mt_cached_pages_to_refresh', array( $receipts, $tickets, $purchase ) );
foreach ( $to_refresh as $post ) {
if ( ! $post || ! get_post( $post ) ) {
continue;
}
// WordPress core.
clean_post_cache( $post );
// W3 Total Cache.
if ( function_exists( 'w3tc_pgcache_flush_post' ) ) {
w3tc_pgcache_flush_post( $post );
}
// WP Super Cache.
if ( function_exists( 'wp_cache_post_change' ) ) {
wp_cache_post_change( $post );
}
// WP Rocket.
if ( function_exists( 'rocket_clean_post' ) ) {
rocket_clean_post( $post );
}
// WP Fastest Cache.
if ( isset( $GLOBALS['wp_fastest_cache'] ) && method_exists( $GLOBALS['wp_fastest_cache'], 'singleDeleteCache' ) ) {
$GLOBALS['wp_fastest_cache']->singleDeleteCache( false, $post );
}
// Comet Cache.
if ( class_exists( 'comet_cache' ) ) {
comet_cache::clearPost( $post );
}
// Cache Enabler.
if ( class_exists( 'Cache_Enabler' ) ) {
Cache_Enabler::clear_page_cache_by_post_id( $post );
}
// WP-Optimize.
if ( class_exists( 'WPO_Page_Cache' ) ) {
WPO_Page_Cache::delete_single_post_cache( $post );
}
}
wp_cache_flush();
}