Bidirectional sync between Eventbrite and The Events Calendar, with WooCommerce ticket purchasing that bypasses Eventbrite's processing fees by registering buyers as free attendees via API. Includes venue/ organizer sync, QR code ticket generation, attendee management with CSV export, scheduled sync via WP-Cron, and real-time Eventbrite webhooks.
395 lines
13 KiB
PHP
395 lines
13 KiB
PHP
<?php
|
|
if ( ! defined( 'ABSPATH' ) ) {
|
|
exit;
|
|
}
|
|
|
|
final class EB4TEC_WooCommerce {
|
|
|
|
public function __construct(
|
|
private readonly EB4TEC_API_Client $api,
|
|
private readonly EB4TEC_Ticket_Manager $ticket_manager,
|
|
private readonly EB4TEC_QR_Code $qr,
|
|
) {
|
|
add_action( 'woocommerce_order_status_completed', [ $this, 'on_order_completed' ] );
|
|
add_action( 'woocommerce_order_status_refunded', [ $this, 'on_order_refunded' ] );
|
|
add_action( 'woocommerce_order_status_cancelled', [ $this, 'on_order_refunded' ] );
|
|
add_action( 'add_meta_boxes', [ $this, 'add_meta_boxes' ] );
|
|
add_action( 'save_post', [ $this, 'save_product_meta_box' ] );
|
|
add_action( 'woocommerce_thankyou', [ $this, 'show_qr_on_thankyou' ] );
|
|
add_action( 'woocommerce_email_order_details', [ $this, 'inject_qr_into_email' ], 20, 4 );
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// WooCommerce product creation / update
|
|
// ---------------------------------------------------------------------------
|
|
|
|
public function sync_event_product( int $post_id ): int|WP_Error {
|
|
$existing_product_id = (int) get_post_meta( $post_id, '_eb4tec_wp_product_id', true );
|
|
|
|
if ( $existing_product_id ) {
|
|
$this->update_event_product( $existing_product_id, $post_id );
|
|
return $existing_product_id;
|
|
}
|
|
|
|
return $this->create_event_product( $post_id );
|
|
}
|
|
|
|
private function create_event_product( int $post_id ): int|WP_Error {
|
|
$event = get_post( $post_id );
|
|
$title = $event ? $event->post_title . ' — ' . __( 'Tickets', 'eb4tec' ) : __( 'Event Tickets', 'eb4tec' );
|
|
$cost = (float) get_post_meta( $post_id, '_EventCost', true );
|
|
$capacity = (int) get_post_meta( $post_id, '_eb4tec_capacity', true );
|
|
|
|
$product = new WC_Product_Simple();
|
|
$product->set_name( $title );
|
|
$product->set_status( 'publish' );
|
|
$product->set_regular_price( (string) $cost );
|
|
$product->set_price( (string) $cost );
|
|
$product->set_virtual( true );
|
|
$product->set_sold_individually( false );
|
|
|
|
if ( $capacity > 0 ) {
|
|
$product->set_manage_stock( true );
|
|
$product->set_stock_quantity( $capacity );
|
|
$product->set_stock_status( 'instock' );
|
|
$product->set_backorders( 'no' );
|
|
}
|
|
|
|
// Assign ticket category if configured.
|
|
$cat_id = (int) get_option( 'eb4tec_woo_category_id', 0 );
|
|
if ( $cat_id ) {
|
|
$product->set_category_ids( [ $cat_id ] );
|
|
}
|
|
|
|
$product_id = $product->save();
|
|
|
|
if ( ! $product_id ) {
|
|
return new WP_Error( 'eb4tec_wc_product', __( 'Failed to create WooCommerce product.', 'eb4tec' ) );
|
|
}
|
|
|
|
update_post_meta( $product_id, '_eb4tec_event_post_id', $post_id );
|
|
update_post_meta( $product_id, '_eb4tec_is_event_ticket', 'yes' );
|
|
update_post_meta( $post_id, '_eb4tec_wp_product_id', $product_id );
|
|
|
|
// Transfer featured image.
|
|
$thumbnail_id = get_post_thumbnail_id( $post_id );
|
|
if ( $thumbnail_id ) {
|
|
set_post_thumbnail( $product_id, $thumbnail_id );
|
|
}
|
|
|
|
return $product_id;
|
|
}
|
|
|
|
private function update_event_product( int $product_id, int $post_id ): void {
|
|
$product = wc_get_product( $product_id );
|
|
if ( ! $product ) {
|
|
return;
|
|
}
|
|
|
|
$event = get_post( $post_id );
|
|
$title = $event ? $event->post_title . ' — ' . __( 'Tickets', 'eb4tec' ) : $product->get_name();
|
|
$cost = (float) get_post_meta( $post_id, '_EventCost', true );
|
|
$capacity = (int) get_post_meta( $post_id, '_eb4tec_capacity', true );
|
|
|
|
$product->set_name( $title );
|
|
$product->set_regular_price( (string) $cost );
|
|
$product->set_price( (string) $cost );
|
|
|
|
if ( $capacity > 0 ) {
|
|
$product->set_manage_stock( true );
|
|
$product->set_stock_quantity( $capacity );
|
|
}
|
|
|
|
$product->save();
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Order completed: register attendees in Eventbrite
|
|
// ---------------------------------------------------------------------------
|
|
|
|
public function on_order_completed( int $order_id ): void {
|
|
$order = wc_get_order( $order_id );
|
|
if ( ! $order ) {
|
|
return;
|
|
}
|
|
|
|
foreach ( $order->get_items() as $item_id => $item ) {
|
|
$product_id = $item->get_product_id();
|
|
if ( get_post_meta( $product_id, '_eb4tec_is_event_ticket', true ) !== 'yes' ) {
|
|
continue;
|
|
}
|
|
|
|
$post_id = (int) get_post_meta( $product_id, '_eb4tec_event_post_id', true );
|
|
$eb_event_id = (string) get_post_meta( $post_id, '_eb4tec_event_id', true );
|
|
|
|
if ( ! $post_id || ! $eb_event_id ) {
|
|
continue;
|
|
}
|
|
|
|
$capacity = (int) get_post_meta( $post_id, '_eb4tec_capacity', true );
|
|
$class_id_or_error = $this->ticket_manager->ensure_wp_ticket_class( $eb_event_id, $post_id, $capacity );
|
|
if ( is_wp_error( $class_id_or_error ) ) {
|
|
$order->add_order_note( sprintf(
|
|
__( 'EB4TEC: Failed to get ticket class for event %d: %s', 'eb4tec' ),
|
|
$post_id,
|
|
$class_id_or_error->get_error_message()
|
|
) );
|
|
continue;
|
|
}
|
|
|
|
$qty = (int) $item->get_quantity();
|
|
$attendee_ids = [];
|
|
$qr_codes = [];
|
|
|
|
for ( $i = 0; $i < $qty; $i++ ) {
|
|
$attendee_result = $this->register_attendee( $order_id, $order, $eb_event_id, $class_id_or_error, $i );
|
|
|
|
if ( is_wp_error( $attendee_result ) ) {
|
|
$order->add_order_note( sprintf(
|
|
__( 'EB4TEC: Failed to register attendee %d for event %d: %s', 'eb4tec' ),
|
|
$i + 1,
|
|
$post_id,
|
|
$attendee_result->get_error_message()
|
|
) );
|
|
continue;
|
|
}
|
|
|
|
$attendee_ids[] = $attendee_result;
|
|
|
|
// Generate QR code for this attendee.
|
|
$qr_data = $this->qr->get_attendee_qr_data( $attendee_result, (string) $order_id, $eb_event_id );
|
|
$qr_url = $this->qr->generate( $qr_data );
|
|
if ( ! is_wp_error( $qr_url ) ) {
|
|
$qr_codes[] = [
|
|
'attendee_id' => $attendee_result,
|
|
'qr_url' => $qr_url,
|
|
];
|
|
}
|
|
}
|
|
|
|
// Persist to order meta (append to any already registered for other items).
|
|
$existing_ids = json_decode( get_post_meta( $order_id, '_eb4tec_attendee_ids', true ) ?: '[]', true );
|
|
$existing_codes = json_decode( get_post_meta( $order_id, '_eb4tec_qr_codes', true ) ?: '[]', true );
|
|
|
|
update_post_meta( $order_id, '_eb4tec_attendee_ids', wp_json_encode( array_merge( $existing_ids, $attendee_ids ) ) );
|
|
update_post_meta( $order_id, '_eb4tec_qr_codes', wp_json_encode( array_merge( $existing_codes, $qr_codes ) ) );
|
|
update_post_meta( $order_id, '_eb4tec_event_post_id', $post_id );
|
|
|
|
if ( ! empty( $attendee_ids ) ) {
|
|
$order->add_order_note( sprintf(
|
|
__( 'EB4TEC: Registered %d attendee(s) in Eventbrite (IDs: %s).', 'eb4tec' ),
|
|
count( $attendee_ids ),
|
|
implode( ', ', $attendee_ids )
|
|
) );
|
|
}
|
|
}
|
|
}
|
|
|
|
public function on_order_refunded( int $order_id ): void {
|
|
$attendee_ids = json_decode( get_post_meta( $order_id, '_eb4tec_attendee_ids', true ) ?: '[]', true );
|
|
if ( empty( $attendee_ids ) ) {
|
|
return;
|
|
}
|
|
|
|
// Eventbrite's API does not support cancelling free-class attendees via API.
|
|
// Log a note for manual follow-up.
|
|
$order = wc_get_order( $order_id );
|
|
if ( $order ) {
|
|
$order->add_order_note( sprintf(
|
|
/* translators: list of Eventbrite attendee IDs */
|
|
__( 'EB4TEC: Order refunded/cancelled. Please manually cancel these Eventbrite attendees: %s', 'eb4tec' ),
|
|
implode( ', ', array_map( 'esc_html', $attendee_ids ) )
|
|
) );
|
|
}
|
|
}
|
|
|
|
private function register_attendee( int $order_id, WC_Order $order, string $eb_event_id, string $ticket_class_id, int $index ): string|WP_Error {
|
|
$first_name = sanitize_text_field( $order->get_billing_first_name() );
|
|
$last_name = sanitize_text_field( $order->get_billing_last_name() );
|
|
$email = sanitize_email( $order->get_billing_email() );
|
|
|
|
// Append ticket number if ordering multiple.
|
|
$name = trim( "$first_name $last_name" );
|
|
if ( $index > 0 ) {
|
|
$name .= ' (' . ( $index + 1 ) . ')';
|
|
}
|
|
|
|
$result = $this->api->create_attendee( $eb_event_id, [
|
|
'attendee' => [
|
|
'ticket_class_id' => $ticket_class_id,
|
|
'profile' => [
|
|
'name' => $name,
|
|
'email' => $email,
|
|
],
|
|
'answers' => [],
|
|
],
|
|
] );
|
|
|
|
if ( is_wp_error( $result ) ) {
|
|
return $result;
|
|
}
|
|
|
|
$attendee_id = $result['id'] ?? '';
|
|
return $attendee_id ?: new WP_Error( 'eb4tec_no_attendee_id', __( 'Eventbrite did not return an attendee ID.', 'eb4tec' ) );
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Meta boxes
|
|
// ---------------------------------------------------------------------------
|
|
|
|
public function add_meta_boxes(): void {
|
|
// Order meta box.
|
|
add_meta_box(
|
|
'eb4tec_order_meta',
|
|
__( 'Eventbrite Attendees', 'eb4tec' ),
|
|
[ $this, 'render_order_meta_box' ],
|
|
wc_get_page_screen_id( 'shop-order' ),
|
|
'side',
|
|
'default'
|
|
);
|
|
|
|
// Product meta box.
|
|
add_meta_box(
|
|
'eb4tec_product_meta',
|
|
__( 'Linked Eventbrite Event', 'eb4tec' ),
|
|
[ $this, 'render_product_meta_box' ],
|
|
'product',
|
|
'side',
|
|
'default'
|
|
);
|
|
}
|
|
|
|
public function render_order_meta_box( WP_Post $post ): void {
|
|
$attendee_ids = json_decode( get_post_meta( $post->ID, '_eb4tec_attendee_ids', true ) ?: '[]', true );
|
|
$post_id = (int) get_post_meta( $post->ID, '_eb4tec_event_post_id', true );
|
|
$eb_event_id = $post_id ? (string) get_post_meta( $post_id, '_eb4tec_event_id', true ) : '';
|
|
|
|
if ( empty( $attendee_ids ) ) {
|
|
echo '<p class="description">' . esc_html__( 'No Eventbrite attendees registered for this order.', 'eb4tec' ) . '</p>';
|
|
return;
|
|
}
|
|
|
|
echo '<ul style="margin:0;padding:0;list-style:none;">';
|
|
foreach ( $attendee_ids as $id ) {
|
|
echo '<li style="margin-bottom:4px;">';
|
|
if ( $eb_event_id ) {
|
|
printf(
|
|
'<a href="https://www.eventbrite.com/reports?eid=%s" target="_blank">%s</a>',
|
|
esc_attr( $eb_event_id ),
|
|
esc_html( $id )
|
|
);
|
|
} else {
|
|
echo esc_html( $id );
|
|
}
|
|
echo '</li>';
|
|
}
|
|
echo '</ul>';
|
|
}
|
|
|
|
public function render_product_meta_box( WP_Post $post ): void {
|
|
wp_nonce_field( 'eb4tec_product_meta_save', '_eb4tec_product_meta_nonce' );
|
|
|
|
$event_post_id = (int) get_post_meta( $post->ID, '_eb4tec_event_post_id', true );
|
|
$is_ticket = get_post_meta( $post->ID, '_eb4tec_is_event_ticket', true ) === 'yes';
|
|
|
|
if ( $event_post_id ) {
|
|
$event_title = get_the_title( $event_post_id );
|
|
$edit_url = get_edit_post_link( $event_post_id );
|
|
echo '<p>';
|
|
printf(
|
|
esc_html__( 'Ticket for: %s', 'eb4tec' ) . ' <a href="%s">%s</a>',
|
|
esc_html( $event_title ),
|
|
esc_url( $edit_url ),
|
|
esc_html__( 'Edit event', 'eb4tec' )
|
|
);
|
|
echo '</p>';
|
|
} else {
|
|
echo '<p class="description">' . esc_html__( 'Not linked to a TEC event.', 'eb4tec' ) . '</p>';
|
|
}
|
|
|
|
echo '<p><label><input type="hidden" name="eb4tec_is_event_ticket" value="">';
|
|
echo '<input type="checkbox" name="eb4tec_is_event_ticket" value="yes" ' . checked( $is_ticket, true, false ) . '> ';
|
|
esc_html_e( 'This is an event ticket product', 'eb4tec' );
|
|
echo '</label></p>';
|
|
}
|
|
|
|
public function save_product_meta_box( int $post_id ): void {
|
|
if ( get_post_type( $post_id ) !== 'product' ) {
|
|
return;
|
|
}
|
|
|
|
if ( ! wp_verify_nonce( $_POST['_eb4tec_product_meta_nonce'] ?? '', 'eb4tec_product_meta_save' ) ) {
|
|
return;
|
|
}
|
|
|
|
if ( ! current_user_can( 'edit_post', $post_id ) ) {
|
|
return;
|
|
}
|
|
|
|
$is_ticket = ( $_POST['eb4tec_is_event_ticket'] ?? '' ) === 'yes' ? 'yes' : 'no';
|
|
update_post_meta( $post_id, '_eb4tec_is_event_ticket', $is_ticket );
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// QR display: thank-you page and email
|
|
// ---------------------------------------------------------------------------
|
|
|
|
public function show_qr_on_thankyou( int $order_id ): void {
|
|
if ( ! get_option( 'eb4tec_qr_on_order_page' ) ) {
|
|
return;
|
|
}
|
|
|
|
$qr_codes = json_decode( get_post_meta( $order_id, '_eb4tec_qr_codes', true ) ?: '[]', true );
|
|
if ( empty( $qr_codes ) ) {
|
|
return;
|
|
}
|
|
|
|
echo '<section class="woocommerce-order-details eb4tec-ticket-qr">';
|
|
echo '<h2 class="woocommerce-order-details__title">' . esc_html__( 'Your Event Tickets', 'eb4tec' ) . '</h2>';
|
|
echo '<p>' . esc_html__( 'Present these QR codes at the event for check-in.', 'eb4tec' ) . '</p>';
|
|
|
|
foreach ( $qr_codes as $index => $entry ) {
|
|
$attendee_id = $entry['attendee_id'] ?? '';
|
|
$qr_url = $entry['qr_url'] ?? '';
|
|
if ( ! $qr_url ) {
|
|
continue;
|
|
}
|
|
echo '<div class="eb4tec-ticket" style="display:inline-block;margin:8px 16px 8px 0;text-align:center;">';
|
|
echo $this->qr->render_qr_html( $qr_url, $attendee_id );
|
|
echo '<small>' . sprintf( esc_html__( 'Ticket %d', 'eb4tec' ), $index + 1 ) . '</small>';
|
|
echo '</div>';
|
|
}
|
|
|
|
echo '</section>';
|
|
}
|
|
|
|
public function inject_qr_into_email( WC_Order $order, bool $sent_to_admin, bool $plain_text, WC_Email $email ): void {
|
|
if ( $sent_to_admin || $plain_text ) {
|
|
return;
|
|
}
|
|
|
|
if ( ! get_option( 'eb4tec_qr_in_email' ) ) {
|
|
return;
|
|
}
|
|
|
|
$qr_codes = json_decode( get_post_meta( $order->get_id(), '_eb4tec_qr_codes', true ) ?: '[]', true );
|
|
if ( empty( $qr_codes ) ) {
|
|
return;
|
|
}
|
|
|
|
echo '<h2 style="color:#333;font-size:18px;margin-top:24px;">' . esc_html__( 'Your Event Tickets', 'eb4tec' ) . '</h2>';
|
|
echo '<p>' . esc_html__( 'Present these QR codes at the event for check-in.', 'eb4tec' ) . '</p>';
|
|
|
|
foreach ( $qr_codes as $index => $entry ) {
|
|
$qr_url = $entry['qr_url'] ?? '';
|
|
$attendee_id = $entry['attendee_id'] ?? '';
|
|
if ( ! $qr_url ) {
|
|
continue;
|
|
}
|
|
echo '<div style="margin-bottom:16px;">';
|
|
echo '<strong>' . sprintf( esc_html__( 'Ticket %d', 'eb4tec' ), $index + 1 ) . '</strong><br>';
|
|
echo $this->qr->render_qr_html( $qr_url, $attendee_id );
|
|
echo '</div>';
|
|
}
|
|
}
|
|
}
|