eventbrite-for-the-events-c.../includes/class-eb4tec-woocommerce.php
Laurence Horrocks-Barlow f3bc795d9a Initial release of Eventbrite for The Events Calendar (v1.0.0)
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.
2026-05-17 08:48:04 +01:00

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>';
}
}
}