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.
This commit is contained in:
commit
f3bc795d9a
15 changed files with 3132 additions and 0 deletions
64
eb4tec.php
Normal file
64
eb4tec.php
Normal file
|
|
@ -0,0 +1,64 @@
|
|||
<?php
|
||||
/**
|
||||
* Plugin Name: Eventbrite for The Events Calendar
|
||||
* Plugin URI: https://git.ankh-morpork.discworld.network/laurence/eventbrite-for-the-events-calendar
|
||||
* Description: Bidirectional sync between Eventbrite and The Events Calendar, with WooCommerce ticket
|
||||
* purchasing that bypasses Eventbrite's processing fees while keeping Eventbrite as the
|
||||
* attendee source of truth. Includes venue/organizer sync, QR code tickets, attendee
|
||||
* management, scheduled sync, and real-time Eventbrite webhooks.
|
||||
* Version: 1.0.0
|
||||
* Author: Laurence Horrocks-Barlow
|
||||
* Author URI: https://qsplace.co.uk
|
||||
* Text Domain: eb4tec
|
||||
* Domain Path: /languages
|
||||
* License: GPLv2 or later
|
||||
* License URI: https://www.gnu.org/licenses/gpl-2.0.html
|
||||
* Requires at least: 6.0
|
||||
* Requires PHP: 8.0
|
||||
*/
|
||||
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit;
|
||||
}
|
||||
|
||||
define( 'EB4TEC_VERSION', '1.0.0' );
|
||||
define( 'EB4TEC_FILE', __FILE__ );
|
||||
define( 'EB4TEC_DIR', plugin_dir_path( __FILE__ ) );
|
||||
define( 'EB4TEC_URL', plugin_dir_url( __FILE__ ) );
|
||||
|
||||
register_activation_hook( __FILE__, function (): void {
|
||||
require_once EB4TEC_DIR . 'includes/class-eb4tec-cron.php';
|
||||
EB4TEC_Cron::schedule();
|
||||
flush_rewrite_rules();
|
||||
} );
|
||||
|
||||
register_deactivation_hook( __FILE__, function (): void {
|
||||
require_once EB4TEC_DIR . 'includes/class-eb4tec-cron.php';
|
||||
EB4TEC_Cron::unschedule();
|
||||
} );
|
||||
|
||||
register_uninstall_hook( __FILE__, 'eb4tec_uninstall' );
|
||||
|
||||
function eb4tec_uninstall(): void {
|
||||
if ( ! get_option( 'eb4tec_delete_on_uninstall' ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
global $wpdb;
|
||||
|
||||
// Remove all plugin options.
|
||||
$wpdb->query( "DELETE FROM {$wpdb->options} WHERE option_name LIKE 'eb4tec_%'" );
|
||||
|
||||
// Remove all plugin post meta.
|
||||
$wpdb->query( "DELETE FROM {$wpdb->postmeta} WHERE meta_key LIKE '\_eb4tec\_%'" );
|
||||
|
||||
// Remove transients.
|
||||
$wpdb->query( "DELETE FROM {$wpdb->options} WHERE option_name LIKE '_transient_eb4tec_%' OR option_name LIKE '_transient_timeout_eb4tec_%'" );
|
||||
}
|
||||
|
||||
add_action( 'plugins_loaded', function (): void {
|
||||
foreach ( glob( EB4TEC_DIR . 'includes/class-eb4tec-*.php' ) as $file ) {
|
||||
require_once $file;
|
||||
}
|
||||
new EB4TEC_Loader();
|
||||
} );
|
||||
242
includes/class-eb4tec-api-client.php
Normal file
242
includes/class-eb4tec-api-client.php
Normal file
|
|
@ -0,0 +1,242 @@
|
|||
<?php
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit;
|
||||
}
|
||||
|
||||
final class EB4TEC_API_Client {
|
||||
|
||||
const BASE_URL = 'https://www.eventbriteapi.com/v3/';
|
||||
const CACHE_TTL = 300;
|
||||
|
||||
private string $token;
|
||||
private int $rate_limit_remaining = 1000;
|
||||
|
||||
public function __construct() {
|
||||
$this->token = (string) get_option( 'eb4tec_api_token', '' );
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Credentials / user
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
public function get_user_me(): array|WP_Error {
|
||||
return $this->get( 'users/me/', cacheable: false );
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Events
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
public function get_organization_events( string $org_id, int $page = 1 ): array|WP_Error {
|
||||
return $this->get( "organizations/{$org_id}/events/", [
|
||||
'status' => 'draft,live,started,ended,completed',
|
||||
'expand' => 'venue,organizer,ticket_classes',
|
||||
'page' => $page,
|
||||
'page_size' => 50,
|
||||
'order_by' => 'start_asc',
|
||||
] );
|
||||
}
|
||||
|
||||
public function get_event( string $eb_event_id ): array|WP_Error {
|
||||
return $this->get( "events/{$eb_event_id}/", [ 'expand' => 'venue,organizer,ticket_classes' ] );
|
||||
}
|
||||
|
||||
public function create_event( array $data ): array|WP_Error {
|
||||
$org_id = (string) get_option( 'eb4tec_org_id', '' );
|
||||
return $this->post( "organizations/{$org_id}/events/", $data );
|
||||
}
|
||||
|
||||
public function update_event( string $eb_event_id, array $data ): array|WP_Error {
|
||||
return $this->post( "events/{$eb_event_id}/", $data );
|
||||
}
|
||||
|
||||
public function publish_event( string $eb_event_id ): array|WP_Error {
|
||||
return $this->post( "events/{$eb_event_id}/publish/", [] );
|
||||
}
|
||||
|
||||
public function unpublish_event( string $eb_event_id ): array|WP_Error {
|
||||
return $this->post( "events/{$eb_event_id}/unpublish/", [] );
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Ticket classes
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
public function get_ticket_classes( string $eb_event_id ): array|WP_Error {
|
||||
return $this->get( "events/{$eb_event_id}/ticket_classes/" );
|
||||
}
|
||||
|
||||
public function create_ticket_class( string $eb_event_id, array $data ): array|WP_Error {
|
||||
return $this->post( "events/{$eb_event_id}/ticket_classes/", $data );
|
||||
}
|
||||
|
||||
public function update_ticket_class( string $eb_event_id, string $class_id, array $data ): array|WP_Error {
|
||||
return $this->post( "events/{$eb_event_id}/ticket_classes/{$class_id}/", $data );
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Attendees
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
public function create_attendee( string $eb_event_id, array $data ): array|WP_Error {
|
||||
return $this->post( "events/{$eb_event_id}/attendees/", $data );
|
||||
}
|
||||
|
||||
public function get_attendees( string $eb_event_id, int $page = 1 ): array|WP_Error {
|
||||
return $this->get( "events/{$eb_event_id}/attendees/", [
|
||||
'page' => $page,
|
||||
'page_size' => 50,
|
||||
'expand' => 'ticket_class',
|
||||
], cacheable: false );
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Venues
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
public function get_venue( string $venue_id ): array|WP_Error {
|
||||
return $this->get( "venues/{$venue_id}/" );
|
||||
}
|
||||
|
||||
public function create_venue( string $org_id, array $data ): array|WP_Error {
|
||||
return $this->post( "organizations/{$org_id}/venues/", $data );
|
||||
}
|
||||
|
||||
public function update_venue( string $venue_id, array $data ): array|WP_Error {
|
||||
return $this->post( "venues/{$venue_id}/", $data );
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Organizers
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
public function get_organizer( string $organizer_id ): array|WP_Error {
|
||||
return $this->get( "organizers/{$organizer_id}/" );
|
||||
}
|
||||
|
||||
public function create_organizer( string $org_id, array $data ): array|WP_Error {
|
||||
return $this->post( "organizations/{$org_id}/organizers/", $data );
|
||||
}
|
||||
|
||||
public function update_organizer( string $organizer_id, array $data ): array|WP_Error {
|
||||
return $this->post( "organizers/{$organizer_id}/", $data );
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Webhooks
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
public function create_webhook( array $data ): array|WP_Error {
|
||||
$org_id = (string) get_option( 'eb4tec_org_id', '' );
|
||||
return $this->post( "organizations/{$org_id}/webhooks/", $data );
|
||||
}
|
||||
|
||||
public function list_webhooks(): array|WP_Error {
|
||||
$org_id = (string) get_option( 'eb4tec_org_id', '' );
|
||||
return $this->get( "organizations/{$org_id}/webhooks/", cacheable: false );
|
||||
}
|
||||
|
||||
public function delete_webhook( string $webhook_id ): array|WP_Error {
|
||||
return $this->delete_request( "webhooks/{$webhook_id}/" );
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Cache helpers
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
public function bust_cache( string $endpoint ): void {
|
||||
delete_transient( $this->cache_key( $endpoint, [] ) );
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// HTTP internals
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
private function get( string $endpoint, array $query = [], bool $cacheable = true ): array|WP_Error {
|
||||
if ( $cacheable ) {
|
||||
$key = $this->cache_key( $endpoint, $query );
|
||||
$cached = get_transient( $key );
|
||||
if ( false !== $cached ) {
|
||||
return $cached;
|
||||
}
|
||||
}
|
||||
|
||||
$url = self::BASE_URL . $endpoint;
|
||||
$result = $this->request( 'GET', $url, [ 'body' => $query ] );
|
||||
|
||||
if ( ! is_wp_error( $result ) && $cacheable ) {
|
||||
set_transient( $key, $result, self::CACHE_TTL );
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
private function post( string $endpoint, array $body ): array|WP_Error {
|
||||
$url = self::BASE_URL . $endpoint;
|
||||
return $this->request( 'POST', $url, [
|
||||
'body' => wp_json_encode( $body ),
|
||||
'content-type' => 'application/json',
|
||||
] );
|
||||
}
|
||||
|
||||
private function delete_request( string $endpoint ): array|WP_Error {
|
||||
$url = self::BASE_URL . $endpoint;
|
||||
return $this->request( 'DELETE', $url, [] );
|
||||
}
|
||||
|
||||
private function request( string $method, string $url, array $args ): array|WP_Error {
|
||||
if ( empty( $this->token ) ) {
|
||||
return new WP_Error( 'eb4tec_no_token', __( 'Eventbrite API token is not configured.', 'eb4tec' ) );
|
||||
}
|
||||
|
||||
$defaults = [
|
||||
'method' => $method,
|
||||
'timeout' => 15,
|
||||
'headers' => [
|
||||
'Authorization' => 'Bearer ' . $this->token,
|
||||
'Content-Type' => 'application/json',
|
||||
'Accept' => 'application/json',
|
||||
],
|
||||
];
|
||||
|
||||
$args = array_merge( $defaults, $args );
|
||||
$response = wp_remote_request( $url, $args );
|
||||
|
||||
if ( is_wp_error( $response ) ) {
|
||||
return $response;
|
||||
}
|
||||
|
||||
$status = wp_remote_retrieve_response_code( $response );
|
||||
$body = wp_remote_retrieve_body( $response );
|
||||
|
||||
// Track rate limit.
|
||||
$remaining = wp_remote_retrieve_header( $response, 'x-ratelimit-remaining' );
|
||||
if ( '' !== $remaining ) {
|
||||
$this->rate_limit_remaining = (int) $remaining;
|
||||
if ( $this->rate_limit_remaining <= 10 ) {
|
||||
set_transient( 'eb4tec_rate_limit_warning', true, 60 );
|
||||
}
|
||||
}
|
||||
|
||||
$decoded = json_decode( $body, true );
|
||||
|
||||
if ( $status < 200 || $status >= 300 ) {
|
||||
$message = $decoded['error_description'] ?? $decoded['error'] ?? __( 'Unknown Eventbrite API error.', 'eb4tec' );
|
||||
return new WP_Error(
|
||||
'eb4tec_api_error',
|
||||
$message,
|
||||
[ 'status' => $status, 'response' => $decoded ]
|
||||
);
|
||||
}
|
||||
|
||||
return $decoded ?? [];
|
||||
}
|
||||
|
||||
private function cache_key( string $endpoint, array $query ): string {
|
||||
return 'eb4tec_' . md5( $endpoint . serialize( $query ) );
|
||||
}
|
||||
|
||||
public function get_rate_limit_remaining(): int {
|
||||
return $this->rate_limit_remaining;
|
||||
}
|
||||
}
|
||||
278
includes/class-eb4tec-attendees.php
Normal file
278
includes/class-eb4tec-attendees.php
Normal file
|
|
@ -0,0 +1,278 @@
|
|||
<?php
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit;
|
||||
}
|
||||
|
||||
final class EB4TEC_Attendees {
|
||||
|
||||
public function __construct( private readonly EB4TEC_API_Client $api ) {
|
||||
add_action( 'admin_menu', [ $this, 'add_admin_menu' ] );
|
||||
add_action( 'admin_post_eb4tec_export_attendees', [ $this, 'handle_csv_export' ] );
|
||||
}
|
||||
|
||||
public function add_admin_menu(): void {
|
||||
add_submenu_page(
|
||||
'edit.php?post_type=tribe_events',
|
||||
__( 'Attendees (Eventbrite)', 'eb4tec' ),
|
||||
__( 'Attendees (Eventbrite)', 'eb4tec' ),
|
||||
'manage_options',
|
||||
'eb4tec-attendees',
|
||||
[ $this, 'render_attendees_page' ]
|
||||
);
|
||||
}
|
||||
|
||||
public function render_attendees_page(): void {
|
||||
if ( ! current_user_can( 'manage_options' ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$selected_post_id = (int) ( $_GET['event_post_id'] ?? 0 );
|
||||
$page_num = max( 1, (int) ( $_GET['paged'] ?? 1 ) );
|
||||
|
||||
echo '<div class="wrap">';
|
||||
echo '<h1>' . esc_html__( 'Eventbrite Attendees', 'eb4tec' ) . '</h1>';
|
||||
|
||||
$this->render_event_selector( $selected_post_id );
|
||||
|
||||
if ( ! $selected_post_id ) {
|
||||
echo '<p>' . esc_html__( 'Select an event above to view its attendees.', 'eb4tec' ) . '</p>';
|
||||
echo '</div>';
|
||||
return;
|
||||
}
|
||||
|
||||
$eb_event_id = (string) get_post_meta( $selected_post_id, '_eb4tec_event_id', true );
|
||||
if ( ! $eb_event_id ) {
|
||||
echo '<p class="notice notice-warning">' . esc_html__( 'This event is not linked to an Eventbrite event.', 'eb4tec' ) . '</p>';
|
||||
echo '</div>';
|
||||
return;
|
||||
}
|
||||
|
||||
$response = $this->api->get_attendees( $eb_event_id, $page_num );
|
||||
if ( is_wp_error( $response ) ) {
|
||||
echo '<div class="notice notice-error"><p>' . esc_html( $response->get_error_message() ) . '</p></div>';
|
||||
echo '</div>';
|
||||
return;
|
||||
}
|
||||
|
||||
$attendees = $response['attendees'] ?? [];
|
||||
$pagination = $response['pagination'] ?? [];
|
||||
$total = (int) ( $pagination['object_count'] ?? 0 );
|
||||
$page_count = (int) ( $pagination['page_count'] ?? 1 );
|
||||
|
||||
// Export button.
|
||||
echo '<form method="post" action="' . esc_url( admin_url( 'admin-post.php' ) ) . '" style="margin-bottom:12px;">';
|
||||
wp_nonce_field( "eb4tec_export_attendees_{$eb_event_id}", '_eb4tec_export_nonce' );
|
||||
echo '<input type="hidden" name="action" value="eb4tec_export_attendees">';
|
||||
echo '<input type="hidden" name="eb_event_id" value="' . esc_attr( $eb_event_id ) . '">';
|
||||
echo '<input type="hidden" name="event_post_id" value="' . esc_attr( $selected_post_id ) . '">';
|
||||
submit_button( sprintf( __( 'Export All %d Attendees to CSV', 'eb4tec' ), $total ), 'secondary', 'submit', false );
|
||||
echo '</form>';
|
||||
|
||||
$this->render_attendee_table( $attendees, $selected_post_id );
|
||||
$this->render_pagination( $page_num, $page_count, $selected_post_id );
|
||||
|
||||
echo '</div>';
|
||||
}
|
||||
|
||||
private function render_event_selector( int $selected_post_id ): void {
|
||||
$events = get_posts( [
|
||||
'post_type' => 'tribe_events',
|
||||
'post_status' => [ 'publish', 'draft' ],
|
||||
'posts_per_page' => 200,
|
||||
'orderby' => 'title',
|
||||
'order' => 'ASC',
|
||||
'meta_query' => [ [
|
||||
'key' => '_eb4tec_event_id',
|
||||
'compare' => 'EXISTS',
|
||||
] ],
|
||||
] );
|
||||
|
||||
$base_url = admin_url( 'edit.php?post_type=tribe_events&page=eb4tec-attendees' );
|
||||
|
||||
echo '<p>';
|
||||
echo '<label for="eb4tec_event_selector"><strong>' . esc_html__( 'Event:', 'eb4tec' ) . '</strong></label> ';
|
||||
echo '<select id="eb4tec_event_selector" onchange="window.location=this.options[this.selectedIndex].value">';
|
||||
echo '<option value="' . esc_url( $base_url ) . '">' . esc_html__( '— Select an event —', 'eb4tec' ) . '</option>';
|
||||
|
||||
foreach ( $events as $event ) {
|
||||
$url = esc_url( add_query_arg( 'event_post_id', $event->ID, $base_url ) );
|
||||
$label = $event->post_title ?: __( '(untitled)', 'eb4tec' );
|
||||
printf(
|
||||
'<option value="%s" %s>%s</option>',
|
||||
$url,
|
||||
selected( $selected_post_id, $event->ID, false ),
|
||||
esc_html( $label )
|
||||
);
|
||||
}
|
||||
|
||||
echo '</select>';
|
||||
echo '</p>';
|
||||
}
|
||||
|
||||
private function render_attendee_table( array $attendees, int $post_id ): void {
|
||||
if ( empty( $attendees ) ) {
|
||||
echo '<p>' . esc_html__( 'No attendees found for this event.', 'eb4tec' ) . '</p>';
|
||||
return;
|
||||
}
|
||||
|
||||
// Build a map of EB attendee ID → WC order number from stored order meta.
|
||||
$order_map = $this->build_order_map( $post_id );
|
||||
|
||||
echo '<table class="wp-list-table widefat fixed striped">';
|
||||
echo '<thead><tr>';
|
||||
$headers = [
|
||||
__( 'Name', 'eb4tec' ),
|
||||
__( 'Email', 'eb4tec' ),
|
||||
__( 'WC Order #', 'eb4tec' ),
|
||||
__( 'Ticket Class', 'eb4tec' ),
|
||||
__( 'Status', 'eb4tec' ),
|
||||
__( 'Checked In', 'eb4tec' ),
|
||||
__( 'Registered', 'eb4tec' ),
|
||||
];
|
||||
foreach ( $headers as $h ) {
|
||||
echo '<th>' . esc_html( $h ) . '</th>';
|
||||
}
|
||||
echo '</tr></thead><tbody>';
|
||||
|
||||
foreach ( $attendees as $attendee ) {
|
||||
$profile = $attendee['profile'] ?? [];
|
||||
$name = esc_html( $profile['name'] ?? '' );
|
||||
$email = esc_html( $profile['email'] ?? '' );
|
||||
$status = esc_html( $attendee['status'] ?? '' );
|
||||
$checked = ! empty( $attendee['checked_in'] ) ? esc_html__( 'Yes', 'eb4tec' ) : esc_html__( 'No', 'eb4tec' );
|
||||
$tc_name = esc_html( $attendee['ticket_class'] ['name'] ?? '' );
|
||||
$created = esc_html( $attendee['created'] ?? '' );
|
||||
$eb_att_id = $attendee['id'] ?? '';
|
||||
$order_num = isset( $order_map[ $eb_att_id ] ) ? '#' . $order_map[ $eb_att_id ] : '—';
|
||||
|
||||
echo "<tr><td>{$name}</td><td>{$email}</td><td>{$order_num}</td><td>{$tc_name}</td><td>{$status}</td><td>{$checked}</td><td>{$created}</td></tr>";
|
||||
}
|
||||
|
||||
echo '</tbody></table>';
|
||||
}
|
||||
|
||||
private function render_pagination( int $current, int $total_pages, int $post_id ): void {
|
||||
if ( $total_pages <= 1 ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$base_url = add_query_arg( 'event_post_id', $post_id, admin_url( 'edit.php?post_type=tribe_events&page=eb4tec-attendees' ) );
|
||||
|
||||
echo '<div class="tablenav"><div class="tablenav-pages">';
|
||||
for ( $p = 1; $p <= $total_pages; $p++ ) {
|
||||
$url = esc_url( add_query_arg( 'paged', $p, $base_url ) );
|
||||
$class = ( $p === $current ) ? 'button button-primary' : 'button';
|
||||
echo "<a href=\"{$url}\" class=\"{$class}\">{$p}</a> ";
|
||||
}
|
||||
echo '</div></div>';
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// CSV export
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
public function handle_csv_export(): void {
|
||||
if ( ! current_user_can( 'manage_options' ) ) {
|
||||
wp_die( esc_html__( 'Insufficient permissions.', 'eb4tec' ) );
|
||||
}
|
||||
|
||||
$eb_event_id = sanitize_text_field( $_POST['eb_event_id'] ?? '' );
|
||||
$post_id = (int) ( $_POST['event_post_id'] ?? 0 );
|
||||
|
||||
if ( ! $eb_event_id ) {
|
||||
wp_die( esc_html__( 'No event ID provided.', 'eb4tec' ) );
|
||||
}
|
||||
|
||||
check_admin_referer( "eb4tec_export_attendees_{$eb_event_id}", '_eb4tec_export_nonce' );
|
||||
|
||||
$order_map = $this->build_order_map( $post_id );
|
||||
$attendees = $this->fetch_all_attendees( $eb_event_id );
|
||||
|
||||
if ( is_wp_error( $attendees ) ) {
|
||||
wp_die( esc_html( $attendees->get_error_message() ) );
|
||||
}
|
||||
|
||||
$event_title = $post_id ? sanitize_file_name( get_the_title( $post_id ) ) : $eb_event_id;
|
||||
$filename = "attendees-{$event_title}-" . date( 'Y-m-d' ) . '.csv';
|
||||
|
||||
header( 'Content-Type: text/csv; charset=utf-8' );
|
||||
header( 'Content-Disposition: attachment; filename="' . $filename . '"' );
|
||||
header( 'Pragma: no-cache' );
|
||||
header( 'Expires: 0' );
|
||||
|
||||
$output = fopen( 'php://output', 'w' );
|
||||
|
||||
fputcsv( $output, [
|
||||
__( 'Name', 'eb4tec' ),
|
||||
__( 'Email', 'eb4tec' ),
|
||||
__( 'Order Number', 'eb4tec' ),
|
||||
__( 'Ticket Class', 'eb4tec' ),
|
||||
__( 'Status', 'eb4tec' ),
|
||||
__( 'Checked In', 'eb4tec' ),
|
||||
__( 'Registration Date', 'eb4tec' ),
|
||||
] );
|
||||
|
||||
foreach ( $attendees as $attendee ) {
|
||||
$profile = $attendee['profile'] ?? [];
|
||||
$eb_att_id = $attendee['id'] ?? '';
|
||||
$order_num = $order_map[ $eb_att_id ] ?? '';
|
||||
|
||||
fputcsv( $output, [
|
||||
$profile['name'] ?? '',
|
||||
$profile['email'] ?? '',
|
||||
$order_num,
|
||||
$attendee['ticket_class']['name'] ?? '',
|
||||
$attendee['status'] ?? '',
|
||||
! empty( $attendee['checked_in'] ) ? 'Yes' : 'No',
|
||||
$attendee['created'] ?? '',
|
||||
] );
|
||||
}
|
||||
|
||||
fclose( $output );
|
||||
exit;
|
||||
}
|
||||
|
||||
private function fetch_all_attendees( string $eb_event_id ): array|WP_Error {
|
||||
$page = 1;
|
||||
$attendees = [];
|
||||
|
||||
do {
|
||||
$response = $this->api->get_attendees( $eb_event_id, $page );
|
||||
if ( is_wp_error( $response ) ) {
|
||||
return $response;
|
||||
}
|
||||
$attendees = array_merge( $attendees, $response['attendees'] ?? [] );
|
||||
$has_more = ! empty( $response['pagination']['has_more_items'] );
|
||||
$page++;
|
||||
} while ( $has_more );
|
||||
|
||||
return $attendees;
|
||||
}
|
||||
|
||||
/**
|
||||
* Build a map of Eventbrite attendee ID → WooCommerce order number.
|
||||
* Searches orders that have this event's post ID stored in meta.
|
||||
*/
|
||||
private function build_order_map( int $post_id ): array {
|
||||
if ( ! $post_id ) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$orders = wc_get_orders( [
|
||||
'meta_key' => '_eb4tec_event_post_id',
|
||||
'meta_value' => $post_id,
|
||||
'limit' => -1,
|
||||
'return' => 'ids',
|
||||
] );
|
||||
|
||||
$map = [];
|
||||
foreach ( $orders as $order_id ) {
|
||||
$attendee_ids = json_decode( get_post_meta( $order_id, '_eb4tec_attendee_ids', true ) ?: '[]', true );
|
||||
foreach ( $attendee_ids as $att_id ) {
|
||||
$map[ $att_id ] = $order_id;
|
||||
}
|
||||
}
|
||||
|
||||
return $map;
|
||||
}
|
||||
}
|
||||
120
includes/class-eb4tec-cron.php
Normal file
120
includes/class-eb4tec-cron.php
Normal file
|
|
@ -0,0 +1,120 @@
|
|||
<?php
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit;
|
||||
}
|
||||
|
||||
final class EB4TEC_Cron {
|
||||
|
||||
const HOOK = 'eb4tec_scheduled_sync';
|
||||
const SCHEDULE = 'eb4tec_sync';
|
||||
|
||||
public function __construct( private readonly EB4TEC_Event_Sync $event_sync ) {
|
||||
add_filter( 'cron_schedules', [ $this, 'add_cron_interval' ] );
|
||||
add_action( self::HOOK, [ $this, 'run_sync' ] );
|
||||
add_action( 'admin_post_eb4tec_sync_now', [ $this, 'handle_sync_now' ] );
|
||||
add_action( 'admin_notices', [ $this, 'admin_notices' ] );
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Static: called from activation / deactivation hooks and settings save
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
public static function schedule(): void {
|
||||
if ( ! wp_next_scheduled( self::HOOK ) ) {
|
||||
wp_schedule_event( time(), self::SCHEDULE, self::HOOK );
|
||||
}
|
||||
}
|
||||
|
||||
public static function unschedule(): void {
|
||||
$timestamp = wp_next_scheduled( self::HOOK );
|
||||
if ( $timestamp ) {
|
||||
wp_unschedule_event( $timestamp, self::HOOK );
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Cron interval
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
public function add_cron_interval( array $schedules ): array {
|
||||
$interval_key = (string) get_option( 'eb4tec_sync_interval', 'hourly' );
|
||||
|
||||
// Map WP core interval names to seconds; allow custom 'eb4tec_sync' key too.
|
||||
$map = [
|
||||
'hourly' => HOUR_IN_SECONDS,
|
||||
'twicedaily' => 12 * HOUR_IN_SECONDS,
|
||||
'daily' => DAY_IN_SECONDS,
|
||||
];
|
||||
|
||||
$seconds = $map[ $interval_key ] ?? HOUR_IN_SECONDS;
|
||||
|
||||
$schedules[ self::SCHEDULE ] = [
|
||||
'interval' => $seconds,
|
||||
'display' => __( 'Eventbrite Sync', 'eb4tec' ),
|
||||
];
|
||||
|
||||
return $schedules;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Sync execution
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
public function run_sync(): void {
|
||||
$result = $this->event_sync->run_full_sync();
|
||||
|
||||
set_transient( 'eb4tec_last_sync_result', $result, DAY_IN_SECONDS );
|
||||
update_option( 'eb4tec_last_sync_timestamp', time() );
|
||||
}
|
||||
|
||||
public function handle_sync_now(): void {
|
||||
if ( ! current_user_can( 'manage_options' ) ) {
|
||||
wp_die( esc_html__( 'Insufficient permissions.', 'eb4tec' ) );
|
||||
}
|
||||
|
||||
check_admin_referer( 'eb4tec_sync_now', '_eb4tec_sync_nonce' );
|
||||
|
||||
$this->run_sync();
|
||||
|
||||
$user_id = get_current_user_id();
|
||||
set_transient( "eb4tec_sync_now_result_{$user_id}", get_transient( 'eb4tec_last_sync_result' ), 60 );
|
||||
|
||||
$redirect = wp_get_referer() ?: admin_url( 'edit.php?post_type=tribe_events&page=eb4tec-settings' );
|
||||
wp_safe_redirect( $redirect );
|
||||
exit;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Admin notices (sync result — shown on all admin screens)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
public function admin_notices(): void {
|
||||
if ( ! current_user_can( 'manage_options' ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$screen = get_current_screen();
|
||||
// Only show on TEC / EB4TEC admin screens.
|
||||
if ( ! $screen || ! str_contains( $screen->id ?? '', 'tribe_events' ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$user_id = get_current_user_id();
|
||||
$sync_result = get_transient( "eb4tec_sync_now_result_{$user_id}" );
|
||||
if ( ! is_array( $sync_result ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
delete_transient( "eb4tec_sync_now_result_{$user_id}" );
|
||||
|
||||
$msg = sprintf(
|
||||
__( 'Eventbrite sync complete — %d pulled, %d pushed, %d errors.', 'eb4tec' ),
|
||||
(int) ( $sync_result['pulled'] ?? 0 ),
|
||||
(int) ( $sync_result['pushed'] ?? 0 ),
|
||||
(int) ( $sync_result['errors'] ?? 0 )
|
||||
);
|
||||
$class = ( ( $sync_result['errors'] ?? 0 ) > 0 ) ? 'notice-warning' : 'notice-success';
|
||||
|
||||
echo '<div class="notice ' . esc_attr( $class ) . ' is-dismissible"><p>' . esc_html( $msg ) . '</p></div>';
|
||||
}
|
||||
}
|
||||
483
includes/class-eb4tec-event-sync.php
Normal file
483
includes/class-eb4tec-event-sync.php
Normal file
|
|
@ -0,0 +1,483 @@
|
|||
<?php
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit;
|
||||
}
|
||||
|
||||
final class EB4TEC_Event_Sync {
|
||||
|
||||
public function __construct(
|
||||
private readonly EB4TEC_API_Client $api,
|
||||
private readonly EB4TEC_Venue_Sync $venue_sync,
|
||||
private readonly EB4TEC_Organizer_Sync $organizer_sync,
|
||||
private readonly EB4TEC_Ticket_Manager $ticket_manager,
|
||||
private readonly EB4TEC_WooCommerce $woocommerce,
|
||||
) {
|
||||
add_action( 'add_meta_boxes', [ $this, 'add_meta_box' ] );
|
||||
add_action( 'save_post_tribe_events', [ $this, 'on_save_post' ], 20, 3 );
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Full sync entry point
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
public function run_full_sync(): array {
|
||||
$pulled = 0;
|
||||
$pushed = 0;
|
||||
$errors = [];
|
||||
|
||||
$direction = get_option( 'eb4tec_sync_direction', 'both' );
|
||||
|
||||
if ( in_array( $direction, [ 'both', 'eb_to_tec' ], true ) ) {
|
||||
$pull_result = $this->pull_from_eventbrite();
|
||||
if ( is_wp_error( $pull_result ) ) {
|
||||
$errors[] = $pull_result->get_error_message();
|
||||
} else {
|
||||
$pulled = $pull_result;
|
||||
}
|
||||
}
|
||||
|
||||
if ( in_array( $direction, [ 'both', 'tec_to_eb' ], true ) ) {
|
||||
$push_result = $this->push_to_eventbrite();
|
||||
if ( is_wp_error( $push_result ) ) {
|
||||
$errors[] = $push_result->get_error_message();
|
||||
} else {
|
||||
$pushed = $push_result;
|
||||
}
|
||||
}
|
||||
|
||||
return [ 'pulled' => $pulled, 'pushed' => $pushed, 'errors' => count( $errors ) ];
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Pull: Eventbrite → TEC
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
public function pull_from_eventbrite(): int|WP_Error {
|
||||
$org_id = (string) get_option( 'eb4tec_org_id', '' );
|
||||
if ( empty( $org_id ) ) {
|
||||
return new WP_Error( 'eb4tec_no_org', __( 'Eventbrite organization ID not configured.', 'eb4tec' ) );
|
||||
}
|
||||
|
||||
$page = 1;
|
||||
$pulled = 0;
|
||||
|
||||
do {
|
||||
$response = $this->api->get_organization_events( $org_id, $page );
|
||||
if ( is_wp_error( $response ) ) {
|
||||
return $response;
|
||||
}
|
||||
|
||||
$events = $response['events'] ?? [];
|
||||
$pagination = $response['pagination'] ?? [];
|
||||
|
||||
foreach ( $events as $eb_event ) {
|
||||
$result = $this->pull_event_from_data( $eb_event );
|
||||
if ( ! is_wp_error( $result ) && $result > 0 ) {
|
||||
$pulled++;
|
||||
}
|
||||
}
|
||||
|
||||
$has_more = ! empty( $pagination['has_more_items'] );
|
||||
$page++;
|
||||
} while ( $has_more );
|
||||
|
||||
return $pulled;
|
||||
}
|
||||
|
||||
public function pull_event( string $eb_event_id ): int|WP_Error {
|
||||
$data = $this->api->get_event( $eb_event_id );
|
||||
if ( is_wp_error( $data ) ) {
|
||||
return $data;
|
||||
}
|
||||
return $this->pull_event_from_data( $data );
|
||||
}
|
||||
|
||||
private function pull_event_from_data( array $eb_event ): int|WP_Error {
|
||||
$eb_id = $eb_event['id'] ?? '';
|
||||
if ( empty( $eb_id ) ) {
|
||||
return new WP_Error( 'eb4tec_no_id', __( 'Eventbrite event has no ID.', 'eb4tec' ) );
|
||||
}
|
||||
|
||||
$existing_id = $this->find_tec_event( $eb_id );
|
||||
|
||||
// Skip events the user has marked to exclude from sync.
|
||||
if ( $existing_id && get_post_meta( $existing_id, '_eb4tec_exclude_sync', true ) ) {
|
||||
return $existing_id;
|
||||
}
|
||||
|
||||
$post_args = $this->eb_event_to_tec_args( $eb_event );
|
||||
|
||||
if ( $existing_id ) {
|
||||
$post_args['ID'] = $existing_id;
|
||||
// Remove status from update args — don't override draft if admin changed it.
|
||||
unset( $post_args['post_status'] );
|
||||
$post_id = wp_update_post( $post_args, true );
|
||||
} else {
|
||||
$post_id = wp_insert_post( $post_args, true );
|
||||
}
|
||||
|
||||
if ( is_wp_error( $post_id ) ) {
|
||||
return $post_id;
|
||||
}
|
||||
|
||||
$this->update_tec_from_eb( $post_id, $eb_event );
|
||||
|
||||
// Sync venue.
|
||||
if ( get_option( 'eb4tec_sync_venues' ) && ! empty( $eb_event['venue_id'] ) ) {
|
||||
$venue_post_id = $this->venue_sync->pull_venue( $eb_event['venue_id'] );
|
||||
if ( ! is_wp_error( $venue_post_id ) && $venue_post_id > 0 ) {
|
||||
update_post_meta( $post_id, '_EventVenueID', $venue_post_id );
|
||||
}
|
||||
}
|
||||
|
||||
// Sync organizer.
|
||||
if ( get_option( 'eb4tec_sync_organizers' ) && ! empty( $eb_event['organizer_id'] ) ) {
|
||||
$org_post_id = $this->organizer_sync->pull_organizer( $eb_event['organizer_id'] );
|
||||
if ( ! is_wp_error( $org_post_id ) && $org_post_id > 0 ) {
|
||||
update_post_meta( $post_id, '_EventOrganizerID', $org_post_id );
|
||||
}
|
||||
}
|
||||
|
||||
// Ensure WooCommerce product exists.
|
||||
$this->woocommerce->sync_event_product( $post_id );
|
||||
|
||||
// Ensure hidden ticket class exists on Eventbrite.
|
||||
$capacity = (int) get_post_meta( $post_id, '_eb4tec_capacity', true );
|
||||
if ( $capacity > 0 ) {
|
||||
$this->ticket_manager->ensure_wp_ticket_class( $eb_id, $post_id, $capacity );
|
||||
}
|
||||
|
||||
return $post_id;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Push: TEC → Eventbrite
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
public function push_to_eventbrite(): int|WP_Error {
|
||||
// Find events that need pushing: no EB ID yet, or flagged for push on last save.
|
||||
$query = new WP_Query( [
|
||||
'post_type' => 'tribe_events',
|
||||
'post_status' => [ 'publish', 'draft' ],
|
||||
'posts_per_page' => 100,
|
||||
'fields' => 'ids',
|
||||
'meta_query' => [
|
||||
'relation' => 'OR',
|
||||
[
|
||||
'key' => '_eb4tec_event_id',
|
||||
'compare' => 'NOT EXISTS',
|
||||
],
|
||||
[
|
||||
'key' => '_eb4tec_push_on_save',
|
||||
'value' => '1',
|
||||
],
|
||||
],
|
||||
] );
|
||||
|
||||
$pushed = 0;
|
||||
foreach ( $query->posts as $post_id ) {
|
||||
if ( get_post_meta( $post_id, '_eb4tec_exclude_sync', true ) ) {
|
||||
continue;
|
||||
}
|
||||
$result = $this->push_event( $post_id );
|
||||
if ( ! is_wp_error( $result ) ) {
|
||||
$pushed++;
|
||||
}
|
||||
// Clear the flag after processing.
|
||||
delete_post_meta( $post_id, '_eb4tec_push_on_save' );
|
||||
}
|
||||
|
||||
return $pushed;
|
||||
}
|
||||
|
||||
public function push_event( int $post_id ): string|WP_Error {
|
||||
$org_id = (string) get_option( 'eb4tec_org_id', '' );
|
||||
if ( empty( $org_id ) ) {
|
||||
return new WP_Error( 'eb4tec_no_org', __( 'Eventbrite organization ID not configured.', 'eb4tec' ) );
|
||||
}
|
||||
|
||||
$eb_event_id = (string) get_post_meta( $post_id, '_eb4tec_event_id', true );
|
||||
$body = $this->tec_post_to_eb_body( $post_id );
|
||||
|
||||
if ( $eb_event_id ) {
|
||||
$result = $this->api->update_event( $eb_event_id, $body );
|
||||
} else {
|
||||
$result = $this->api->create_event( $body );
|
||||
}
|
||||
|
||||
if ( is_wp_error( $result ) ) {
|
||||
return $result;
|
||||
}
|
||||
|
||||
$new_eb_id = $result['id'] ?? '';
|
||||
if ( empty( $new_eb_id ) ) {
|
||||
return new WP_Error( 'eb4tec_no_event_id', __( 'Eventbrite did not return an event ID.', 'eb4tec' ) );
|
||||
}
|
||||
|
||||
update_post_meta( $post_id, '_eb4tec_event_id', sanitize_text_field( $new_eb_id ) );
|
||||
update_post_meta( $post_id, '_eb4tec_eb_url', esc_url_raw( $result['url'] ?? '' ) );
|
||||
update_post_meta( $post_id, '_eb4tec_last_synced', time() );
|
||||
|
||||
// Auto-publish if configured.
|
||||
if ( get_option( 'eb4tec_auto_publish' ) && get_post_status( $post_id ) === 'publish' ) {
|
||||
$this->api->publish_event( $new_eb_id );
|
||||
}
|
||||
|
||||
// Push venue.
|
||||
if ( get_option( 'eb4tec_sync_venues' ) ) {
|
||||
$venue_id = (int) get_post_meta( $post_id, '_EventVenueID', true );
|
||||
if ( $venue_id ) {
|
||||
$this->venue_sync->push_venue( $venue_id );
|
||||
}
|
||||
}
|
||||
|
||||
// Push organizer.
|
||||
if ( get_option( 'eb4tec_sync_organizers' ) ) {
|
||||
$org_post_id = (int) get_post_meta( $post_id, '_EventOrganizerID', true );
|
||||
if ( $org_post_id ) {
|
||||
$this->organizer_sync->push_organizer( $org_post_id );
|
||||
}
|
||||
}
|
||||
|
||||
// Create WC product if not already linked.
|
||||
$this->woocommerce->sync_event_product( $post_id );
|
||||
|
||||
// Create hidden ticket class on Eventbrite.
|
||||
$capacity = (int) get_post_meta( $post_id, '_eb4tec_capacity', true );
|
||||
if ( $capacity > 0 ) {
|
||||
$this->ticket_manager->ensure_wp_ticket_class( $new_eb_id, $post_id, $capacity );
|
||||
}
|
||||
|
||||
return $new_eb_id;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Meta box
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
public function add_meta_box(): void {
|
||||
add_meta_box(
|
||||
'eb4tec_event_meta',
|
||||
__( 'Eventbrite Sync', 'eb4tec' ),
|
||||
[ $this, 'render_meta_box' ],
|
||||
'tribe_events',
|
||||
'side',
|
||||
'default'
|
||||
);
|
||||
}
|
||||
|
||||
public function render_meta_box( WP_Post $post ): void {
|
||||
wp_nonce_field( 'eb4tec_event_meta_save', '_eb4tec_event_meta_nonce' );
|
||||
|
||||
$eb_id = esc_attr( get_post_meta( $post->ID, '_eb4tec_event_id', true ) );
|
||||
$eb_url = esc_url( get_post_meta( $post->ID, '_eb4tec_eb_url', true ) );
|
||||
$last_synced = (int) get_post_meta( $post->ID, '_eb4tec_last_synced', true );
|
||||
$eb_status = esc_html( get_post_meta( $post->ID, '_eb4tec_eb_status', true ) );
|
||||
$push_on_save = (bool) get_post_meta( $post->ID, '_eb4tec_push_on_save', true );
|
||||
$exclude = (bool) get_post_meta( $post->ID, '_eb4tec_exclude_sync', true );
|
||||
$capacity = (int) get_post_meta( $post->ID, '_eb4tec_capacity', true );
|
||||
?>
|
||||
<p>
|
||||
<label for="eb4tec_event_id"><strong><?php esc_html_e( 'Eventbrite Event ID', 'eb4tec' ); ?></strong></label><br>
|
||||
<input type="text" id="eb4tec_event_id" name="eb4tec_event_id"
|
||||
value="<?php echo $eb_id; ?>" class="widefat" placeholder="e.g. 123456789">
|
||||
</p>
|
||||
|
||||
<?php if ( $eb_url ) : ?>
|
||||
<p><a href="<?php echo $eb_url; ?>" target="_blank" rel="noopener"><?php esc_html_e( 'View on Eventbrite', 'eb4tec' ); ?> ↗</a></p>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if ( $last_synced ) : ?>
|
||||
<p class="description">
|
||||
<?php printf( esc_html__( 'Last synced: %s ago', 'eb4tec' ), esc_html( human_time_diff( $last_synced ) ) ); ?>
|
||||
<?php if ( $eb_status ) : ?>
|
||||
| <?php printf( esc_html__( 'EB status: %s', 'eb4tec' ), $eb_status ); ?>
|
||||
<?php endif; ?>
|
||||
</p>
|
||||
<?php endif; ?>
|
||||
|
||||
<p>
|
||||
<label for="eb4tec_capacity"><strong><?php esc_html_e( 'Ticket Capacity', 'eb4tec' ); ?></strong></label><br>
|
||||
<input type="number" id="eb4tec_capacity" name="eb4tec_capacity"
|
||||
value="<?php echo esc_attr( $capacity ); ?>" min="0" class="widefat">
|
||||
<span class="description"><?php esc_html_e( 'Leave 0 for unlimited.', 'eb4tec' ); ?></span>
|
||||
</p>
|
||||
|
||||
<p>
|
||||
<label>
|
||||
<input type="checkbox" name="eb4tec_push_on_save" value="1" <?php checked( $push_on_save ); ?>>
|
||||
<?php esc_html_e( 'Push to Eventbrite when saved', 'eb4tec' ); ?>
|
||||
</label>
|
||||
</p>
|
||||
<p>
|
||||
<label>
|
||||
<input type="checkbox" name="eb4tec_exclude_sync" value="1" <?php checked( $exclude ); ?>>
|
||||
<?php esc_html_e( 'Exclude from automatic sync', 'eb4tec' ); ?>
|
||||
</label>
|
||||
</p>
|
||||
<?php
|
||||
}
|
||||
|
||||
public function on_save_post( int $post_id, WP_Post $post, bool $update ): void {
|
||||
if ( ! wp_verify_nonce( $_POST['_eb4tec_event_meta_nonce'] ?? '', 'eb4tec_event_meta_save' ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ( ! current_user_can( 'edit_post', $post_id ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Save meta box fields.
|
||||
$eb_id = sanitize_text_field( $_POST['eb4tec_event_id'] ?? '' );
|
||||
update_post_meta( $post_id, '_eb4tec_event_id', $eb_id );
|
||||
|
||||
$capacity = absint( $_POST['eb4tec_capacity'] ?? 0 );
|
||||
update_post_meta( $post_id, '_eb4tec_capacity', $capacity );
|
||||
|
||||
$push = isset( $_POST['eb4tec_push_on_save'] );
|
||||
$exclude = isset( $_POST['eb4tec_exclude_sync'] );
|
||||
update_post_meta( $post_id, '_eb4tec_push_on_save', $push ? '1' : '' );
|
||||
update_post_meta( $post_id, '_eb4tec_exclude_sync', $exclude ? '1' : '' );
|
||||
|
||||
// Push to Eventbrite now if requested.
|
||||
if ( $push && ! $exclude ) {
|
||||
// Avoid recursion: remove hook, push, re-add.
|
||||
remove_action( 'save_post_tribe_events', [ $this, 'on_save_post' ], 20 );
|
||||
$this->push_event( $post_id );
|
||||
add_action( 'save_post_tribe_events', [ $this, 'on_save_post' ], 20, 3 );
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Helper: find TEC post by EB event ID
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
public function find_tec_event( string $eb_event_id ): int {
|
||||
$query = new WP_Query( [
|
||||
'post_type' => 'tribe_events',
|
||||
'post_status' => [ 'publish', 'draft', 'private' ],
|
||||
'posts_per_page' => 1,
|
||||
'fields' => 'ids',
|
||||
'meta_query' => [ [
|
||||
'key' => '_eb4tec_event_id',
|
||||
'value' => $eb_event_id,
|
||||
] ],
|
||||
] );
|
||||
|
||||
return $query->posts[0] ?? 0;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Field mapping
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
private function eb_event_to_tec_args( array $eb_event ): array {
|
||||
$status = $eb_event['status'] ?? 'draft';
|
||||
$post_status = in_array( $status, [ 'live', 'started', 'ended', 'completed' ], true ) ? 'publish' : 'draft';
|
||||
|
||||
return [
|
||||
'post_title' => sanitize_text_field( $eb_event['name']['text'] ?? __( 'Untitled Event', 'eb4tec' ) ),
|
||||
'post_content' => wp_kses_post( $eb_event['description']['html'] ?? '' ),
|
||||
'post_type' => 'tribe_events',
|
||||
'post_status' => $post_status,
|
||||
];
|
||||
}
|
||||
|
||||
private function update_tec_from_eb( int $post_id, array $eb_event ): void {
|
||||
update_post_meta( $post_id, '_eb4tec_event_id', sanitize_text_field( $eb_event['id'] ) );
|
||||
update_post_meta( $post_id, '_eb4tec_eb_url', esc_url_raw( $eb_event['url'] ?? '' ) );
|
||||
update_post_meta( $post_id, '_eb4tec_eb_status', sanitize_text_field( $eb_event['status'] ?? '' ) );
|
||||
update_post_meta( $post_id, '_eb4tec_last_synced', time() );
|
||||
|
||||
// Dates — stored in local time in TEC.
|
||||
$start_local = sanitize_text_field( $eb_event['start']['local'] ?? '' );
|
||||
$end_local = sanitize_text_field( $eb_event['end']['local'] ?? '' );
|
||||
$timezone = sanitize_text_field( $eb_event['start']['timezone'] ?? '' );
|
||||
|
||||
if ( $start_local ) {
|
||||
update_post_meta( $post_id, '_EventStartDate', $start_local );
|
||||
}
|
||||
if ( $end_local ) {
|
||||
update_post_meta( $post_id, '_EventEndDate', $end_local );
|
||||
}
|
||||
if ( $timezone ) {
|
||||
update_post_meta( $post_id, '_EventTimezone', $timezone );
|
||||
}
|
||||
|
||||
// Capacity.
|
||||
$capacity = (int) ( $eb_event['capacity'] ?? 0 );
|
||||
if ( $capacity > 0 ) {
|
||||
update_post_meta( $post_id, '_eb4tec_capacity', $capacity );
|
||||
update_post_meta( $post_id, '_EventCapacity', $capacity );
|
||||
}
|
||||
|
||||
// Cost — try to get from ticket classes.
|
||||
$ticket_classes = $eb_event['ticket_classes'] ?? [];
|
||||
$min_cost = null;
|
||||
foreach ( $ticket_classes as $tc ) {
|
||||
if ( ! empty( $tc['free'] ) ) {
|
||||
continue;
|
||||
}
|
||||
$cost = (float) ( $tc['cost']['major_value'] ?? 0 );
|
||||
if ( null === $min_cost || $cost < $min_cost ) {
|
||||
$min_cost = $cost;
|
||||
}
|
||||
}
|
||||
if ( null !== $min_cost ) {
|
||||
update_post_meta( $post_id, '_EventCost', (string) $min_cost );
|
||||
update_post_meta( $post_id, '_EventCurrencySymbol', sanitize_text_field( $ticket_classes[0]['cost']['currency'] ?? 'GBP' ) );
|
||||
}
|
||||
|
||||
// Featured image.
|
||||
if ( ! has_post_thumbnail( $post_id ) && ! empty( $eb_event['logo']['url'] ) ) {
|
||||
$this->sideload_image( $eb_event['logo']['url'], $post_id, get_the_title( $post_id ) );
|
||||
}
|
||||
}
|
||||
|
||||
private function tec_post_to_eb_body( int $post_id ): array {
|
||||
$post = get_post( $post_id );
|
||||
$start = (string) get_post_meta( $post_id, '_EventStartDate', true );
|
||||
$end = (string) get_post_meta( $post_id, '_EventEndDate', true );
|
||||
$timezone = (string) get_post_meta( $post_id, '_EventTimezone', true ) ?: wp_timezone_string();
|
||||
$currency = (string) get_post_meta( $post_id, '_EventCurrencySymbol', true ) ?: 'GBP';
|
||||
|
||||
// Convert local dates to UTC for the EB API.
|
||||
$tz_obj = new DateTimeZone( $timezone );
|
||||
$start_dt = new DateTime( $start, $tz_obj );
|
||||
$end_dt = new DateTime( $end, $tz_obj );
|
||||
$start_dt->setTimezone( new DateTimeZone( 'UTC' ) );
|
||||
$end_dt->setTimezone( new DateTimeZone( 'UTC' ) );
|
||||
|
||||
return [
|
||||
'event' => [
|
||||
'name' => [ 'html' => $post->post_title ],
|
||||
'description' => [ 'html' => $post->post_content ],
|
||||
'start' => [
|
||||
'timezone' => $timezone,
|
||||
'utc' => $start_dt->format( 'Y-m-d\TH:i:s\Z' ),
|
||||
],
|
||||
'end' => [
|
||||
'timezone' => $timezone,
|
||||
'utc' => $end_dt->format( 'Y-m-d\TH:i:s\Z' ),
|
||||
],
|
||||
'currency' => $currency,
|
||||
'online_event' => false,
|
||||
'listed' => true,
|
||||
'shareable' => true,
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
private function sideload_image( string $url, int $post_id, string $title ): void {
|
||||
if ( ! function_exists( 'media_sideload_image' ) ) {
|
||||
require_once ABSPATH . 'wp-admin/includes/media.php';
|
||||
require_once ABSPATH . 'wp-admin/includes/file.php';
|
||||
require_once ABSPATH . 'wp-admin/includes/image.php';
|
||||
}
|
||||
|
||||
$attachment_id = media_sideload_image( $url, $post_id, $title, 'id' );
|
||||
if ( ! is_wp_error( $attachment_id ) ) {
|
||||
set_post_thumbnail( $post_id, $attachment_id );
|
||||
}
|
||||
}
|
||||
}
|
||||
196
includes/class-eb4tec-frontend.php
Normal file
196
includes/class-eb4tec-frontend.php
Normal file
|
|
@ -0,0 +1,196 @@
|
|||
<?php
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit;
|
||||
}
|
||||
|
||||
final class EB4TEC_Frontend {
|
||||
|
||||
public function __construct( private readonly EB4TEC_Ticket_Manager $ticket_manager ) {
|
||||
add_action( 'tribe_events_single_event_after_the_meta', [ $this, 'after_event_meta' ] );
|
||||
add_action( 'wp_enqueue_scripts', [ $this, 'enqueue_styles' ] );
|
||||
}
|
||||
|
||||
public function after_event_meta(): void {
|
||||
$post_id = get_the_ID();
|
||||
if ( ! $post_id ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$eb_event_id = (string) get_post_meta( $post_id, '_eb4tec_event_id', true );
|
||||
$product_id = (int) get_post_meta( $post_id, '_eb4tec_wp_product_id', true );
|
||||
$eb_url = (string) get_post_meta( $post_id, '_eb4tec_eb_url', true );
|
||||
|
||||
// Don't render if there's no Eventbrite link and no WC product.
|
||||
if ( ! $eb_event_id && ! $product_id ) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Determine if event has ended.
|
||||
$end_date = (string) get_post_meta( $post_id, '_EventEndDate', true );
|
||||
$timezone = (string) get_post_meta( $post_id, '_EventTimezone', true ) ?: wp_timezone_string();
|
||||
$has_ended = false;
|
||||
if ( $end_date ) {
|
||||
try {
|
||||
$tz_obj = new DateTimeZone( $timezone );
|
||||
$end_dt = new DateTime( $end_date, $tz_obj );
|
||||
$now = new DateTime( 'now', $tz_obj );
|
||||
$has_ended = $now > $end_dt;
|
||||
} catch ( \Exception ) {
|
||||
// Keep $has_ended = false.
|
||||
}
|
||||
}
|
||||
|
||||
echo '<div class="eb4tec-ticket-section">';
|
||||
|
||||
if ( $product_id && ! $has_ended ) {
|
||||
$this->render_capacity_bar( $post_id );
|
||||
$this->render_buy_button( $post_id, $product_id );
|
||||
} elseif ( $has_ended ) {
|
||||
echo '<p class="eb4tec-event-ended">' . esc_html__( 'This event has ended.', 'eb4tec' ) . '</p>';
|
||||
}
|
||||
|
||||
if ( $eb_url ) {
|
||||
echo '<p class="eb4tec-eb-link"><a href="' . esc_url( $eb_url ) . '" target="_blank" rel="noopener">' .
|
||||
esc_html__( 'View on Eventbrite', 'eb4tec' ) . ' ↗</a></p>';
|
||||
}
|
||||
|
||||
echo '</div>';
|
||||
}
|
||||
|
||||
private function render_capacity_bar( int $post_id ): void {
|
||||
$total = (int) get_post_meta( $post_id, '_eb4tec_capacity', true );
|
||||
$available = $this->ticket_manager->get_available_capacity( $post_id );
|
||||
|
||||
if ( $total <= 0 ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$sold = max( 0, $total - $available );
|
||||
$percent = (int) round( ( $sold / $total ) * 100 );
|
||||
|
||||
echo '<div class="eb4tec-capacity-wrap">';
|
||||
printf(
|
||||
'<div class="eb4tec-capacity-bar" role="progressbar" aria-valuenow="%d" aria-valuemin="0" aria-valuemax="%d"><div class="eb4tec-capacity-bar__fill" style="width:%d%%"></div></div>',
|
||||
$sold,
|
||||
$total,
|
||||
$percent
|
||||
);
|
||||
|
||||
if ( $available > 0 ) {
|
||||
echo '<p class="eb4tec-capacity-label">' . esc_html( sprintf(
|
||||
/* translators: 1: available spots, 2: total */
|
||||
_n( '%1$d of %2$d spot remaining', '%1$d of %2$d spots remaining', $available, 'eb4tec' ),
|
||||
$available,
|
||||
$total
|
||||
) ) . '</p>';
|
||||
} else {
|
||||
echo '<p class="eb4tec-capacity-label eb4tec-capacity-label--sold-out">' . esc_html__( 'Sold out', 'eb4tec' ) . '</p>';
|
||||
}
|
||||
|
||||
echo '</div>';
|
||||
}
|
||||
|
||||
private function render_buy_button( int $post_id, int $product_id ): void {
|
||||
$available = $this->ticket_manager->get_available_capacity( $post_id );
|
||||
$button_html = '';
|
||||
|
||||
if ( $available > 0 ) {
|
||||
$product_url = get_permalink( $product_id );
|
||||
$button_html = sprintf(
|
||||
'<a href="%s" class="button eb4tec-buy-button">%s</a>',
|
||||
esc_url( $product_url ),
|
||||
esc_html__( 'Buy Tickets', 'eb4tec' )
|
||||
);
|
||||
} else {
|
||||
$button_html = '<span class="eb4tec-sold-out-badge">' . esc_html__( 'Sold Out', 'eb4tec' ) . '</span>';
|
||||
}
|
||||
|
||||
echo apply_filters( 'eb4tec_ticket_button_html', $button_html, $post_id );
|
||||
}
|
||||
|
||||
public function enqueue_styles(): void {
|
||||
if ( ! is_singular( 'tribe_events' ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
wp_register_style( 'eb4tec-frontend', false );
|
||||
wp_enqueue_style( 'eb4tec-frontend' );
|
||||
wp_add_inline_style( 'eb4tec-frontend', $this->get_inline_css() );
|
||||
}
|
||||
|
||||
private function get_inline_css(): string {
|
||||
return '
|
||||
.eb4tec-ticket-section {
|
||||
margin: 20px 0;
|
||||
padding: 16px 0;
|
||||
border-top: 1px solid #e5e5e5;
|
||||
}
|
||||
.eb4tec-capacity-wrap {
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
.eb4tec-capacity-bar {
|
||||
background: #e5e5e5;
|
||||
border-radius: 4px;
|
||||
height: 8px;
|
||||
overflow: hidden;
|
||||
max-width: 300px;
|
||||
}
|
||||
.eb4tec-capacity-bar__fill {
|
||||
background: #2271b1;
|
||||
height: 100%;
|
||||
transition: width 0.3s ease;
|
||||
}
|
||||
.eb4tec-capacity-label {
|
||||
font-size: 0.875em;
|
||||
color: #555;
|
||||
margin: 4px 0 0;
|
||||
}
|
||||
.eb4tec-capacity-label--sold-out {
|
||||
color: #d63638;
|
||||
font-weight: 600;
|
||||
}
|
||||
.eb4tec-buy-button {
|
||||
display: inline-block;
|
||||
padding: 10px 20px;
|
||||
background: #2271b1;
|
||||
color: #fff !important;
|
||||
text-decoration: none;
|
||||
border-radius: 4px;
|
||||
font-weight: 600;
|
||||
margin-right: 8px;
|
||||
}
|
||||
.eb4tec-buy-button:hover {
|
||||
background: #135e96;
|
||||
}
|
||||
.eb4tec-sold-out-badge {
|
||||
display: inline-block;
|
||||
padding: 8px 16px;
|
||||
background: #d63638;
|
||||
color: #fff;
|
||||
border-radius: 4px;
|
||||
font-weight: 600;
|
||||
}
|
||||
.eb4tec-event-ended {
|
||||
color: #555;
|
||||
font-style: italic;
|
||||
}
|
||||
.eb4tec-eb-link {
|
||||
margin-top: 8px;
|
||||
font-size: 0.875em;
|
||||
}
|
||||
.eb4tec-ticket-classes {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
margin: 0 0 12px;
|
||||
}
|
||||
.eb4tec-ticket-class {
|
||||
padding: 6px 0;
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
}
|
||||
.eb4tec-ticket-sold-out {
|
||||
color: #d63638;
|
||||
font-size: 0.85em;
|
||||
}
|
||||
';
|
||||
}
|
||||
}
|
||||
52
includes/class-eb4tec-loader.php
Normal file
52
includes/class-eb4tec-loader.php
Normal file
|
|
@ -0,0 +1,52 @@
|
|||
<?php
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit;
|
||||
}
|
||||
|
||||
final class EB4TEC_Loader {
|
||||
|
||||
public function __construct() {
|
||||
if ( ! class_exists( 'Tribe__Events__Main' ) ) {
|
||||
add_action( 'admin_notices', [ $this, 'notice_tec_missing' ] );
|
||||
return;
|
||||
}
|
||||
|
||||
if ( ! class_exists( 'WooCommerce' ) ) {
|
||||
add_action( 'admin_notices', [ $this, 'notice_woo_missing' ] );
|
||||
return;
|
||||
}
|
||||
|
||||
$settings = new EB4TEC_Settings();
|
||||
$api = new EB4TEC_API_Client();
|
||||
|
||||
$venue_sync = new EB4TEC_Venue_Sync( $api );
|
||||
$org_sync = new EB4TEC_Organizer_Sync( $api );
|
||||
$ticket_manager = new EB4TEC_Ticket_Manager( $api );
|
||||
$qr = new EB4TEC_QR_Code();
|
||||
$woocommerce = new EB4TEC_WooCommerce( $api, $ticket_manager, $qr );
|
||||
$event_sync = new EB4TEC_Event_Sync( $api, $venue_sync, $org_sync, $ticket_manager, $woocommerce );
|
||||
$cron = new EB4TEC_Cron( $event_sync );
|
||||
$attendees = new EB4TEC_Attendees( $api );
|
||||
$webhook = new EB4TEC_Webhook( $api, $event_sync );
|
||||
$shortcodes = new EB4TEC_Shortcodes( $api, $ticket_manager );
|
||||
$frontend = new EB4TEC_Frontend( $ticket_manager );
|
||||
}
|
||||
|
||||
public function notice_tec_missing(): void {
|
||||
echo '<div class="notice notice-error"><p>' .
|
||||
wp_kses(
|
||||
__( '<strong>Eventbrite for The Events Calendar</strong> requires <a href="https://theeventscalendar.com/" target="_blank">The Events Calendar</a> to be installed and active.', 'eb4tec' ),
|
||||
[ 'strong' => [], 'a' => [ 'href' => [], 'target' => [] ] ]
|
||||
) .
|
||||
'</p></div>';
|
||||
}
|
||||
|
||||
public function notice_woo_missing(): void {
|
||||
echo '<div class="notice notice-error"><p>' .
|
||||
wp_kses(
|
||||
__( '<strong>Eventbrite for The Events Calendar</strong> requires <a href="https://woocommerce.com/" target="_blank">WooCommerce</a> to be installed and active.', 'eb4tec' ),
|
||||
[ 'strong' => [], 'a' => [ 'href' => [], 'target' => [] ] ]
|
||||
) .
|
||||
'</p></div>';
|
||||
}
|
||||
}
|
||||
129
includes/class-eb4tec-organizer-sync.php
Normal file
129
includes/class-eb4tec-organizer-sync.php
Normal file
|
|
@ -0,0 +1,129 @@
|
|||
<?php
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit;
|
||||
}
|
||||
|
||||
final class EB4TEC_Organizer_Sync {
|
||||
|
||||
public function __construct( private readonly EB4TEC_API_Client $api ) {}
|
||||
|
||||
public function pull_organizer( string $eb_organizer_id ): int|WP_Error {
|
||||
if ( empty( $eb_organizer_id ) ) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
$result = $this->api->get_organizer( $eb_organizer_id );
|
||||
if ( is_wp_error( $result ) ) {
|
||||
return $result;
|
||||
}
|
||||
|
||||
$existing_id = $this->find_tec_organizer( $eb_organizer_id );
|
||||
|
||||
$name = sanitize_text_field( $result['name'] ?? '' );
|
||||
|
||||
$post_args = [
|
||||
'post_title' => $name ?: __( 'Untitled Organizer', 'eb4tec' ),
|
||||
'post_content' => wp_kses_post( $result['description']['html'] ?? '' ),
|
||||
'post_type' => 'tribe_organizer',
|
||||
'post_status' => 'publish',
|
||||
];
|
||||
|
||||
if ( $existing_id ) {
|
||||
$post_args['ID'] = $existing_id;
|
||||
$post_id = wp_update_post( $post_args, true );
|
||||
} else {
|
||||
$post_id = wp_insert_post( $post_args, true );
|
||||
}
|
||||
|
||||
if ( is_wp_error( $post_id ) ) {
|
||||
return $post_id;
|
||||
}
|
||||
|
||||
update_post_meta( $post_id, '_eb4tec_organizer_id', sanitize_text_field( $eb_organizer_id ) );
|
||||
|
||||
if ( ! empty( $result['website'] ) ) {
|
||||
update_post_meta( $post_id, '_OrganizerWebsite', esc_url_raw( $result['website'] ) );
|
||||
}
|
||||
|
||||
if ( ! empty( $result['twitter'] ) ) {
|
||||
update_post_meta( $post_id, '_OrganizerTwitter', sanitize_text_field( $result['twitter'] ) );
|
||||
}
|
||||
|
||||
// Sideload logo if not already set.
|
||||
if ( ! has_post_thumbnail( $post_id ) && ! empty( $result['logo']['url'] ) ) {
|
||||
$this->sideload_image( $result['logo']['url'], $post_id, $name );
|
||||
}
|
||||
|
||||
return $post_id;
|
||||
}
|
||||
|
||||
public function push_organizer( int $post_id ): string|WP_Error {
|
||||
$org_id = (string) get_option( 'eb4tec_org_id', '' );
|
||||
if ( empty( $org_id ) ) {
|
||||
return new WP_Error( 'eb4tec_no_org', __( 'Eventbrite organization ID not configured.', 'eb4tec' ) );
|
||||
}
|
||||
|
||||
$eb_organizer_id = get_post_meta( $post_id, '_eb4tec_organizer_id', true );
|
||||
$post = get_post( $post_id );
|
||||
|
||||
if ( ! $post ) {
|
||||
return new WP_Error( 'eb4tec_no_post', __( 'Organizer post not found.', 'eb4tec' ) );
|
||||
}
|
||||
|
||||
$website = (string) get_post_meta( $post_id, '_OrganizerWebsite', true );
|
||||
$twitter = (string) get_post_meta( $post_id, '_OrganizerTwitter', true );
|
||||
|
||||
$body = [
|
||||
'organizer' => [
|
||||
'name' => $post->post_title,
|
||||
'website' => $website,
|
||||
'twitter' => $twitter,
|
||||
],
|
||||
];
|
||||
|
||||
if ( $eb_organizer_id ) {
|
||||
$result = $this->api->update_organizer( $eb_organizer_id, $body );
|
||||
} else {
|
||||
$result = $this->api->create_organizer( $org_id, $body );
|
||||
}
|
||||
|
||||
if ( is_wp_error( $result ) ) {
|
||||
return $result;
|
||||
}
|
||||
|
||||
$new_id = $result['id'] ?? '';
|
||||
if ( $new_id ) {
|
||||
update_post_meta( $post_id, '_eb4tec_organizer_id', sanitize_text_field( $new_id ) );
|
||||
}
|
||||
|
||||
return $new_id ?: new WP_Error( 'eb4tec_no_organizer_id', __( 'Eventbrite did not return an organizer ID.', 'eb4tec' ) );
|
||||
}
|
||||
|
||||
public function find_tec_organizer( string $eb_organizer_id ): int {
|
||||
$query = new WP_Query( [
|
||||
'post_type' => 'tribe_organizer',
|
||||
'post_status' => 'publish',
|
||||
'posts_per_page' => 1,
|
||||
'fields' => 'ids',
|
||||
'meta_query' => [ [
|
||||
'key' => '_eb4tec_organizer_id',
|
||||
'value' => $eb_organizer_id,
|
||||
] ],
|
||||
] );
|
||||
|
||||
return $query->posts[0] ?? 0;
|
||||
}
|
||||
|
||||
private function sideload_image( string $url, int $post_id, string $title ): void {
|
||||
if ( ! function_exists( 'media_sideload_image' ) ) {
|
||||
require_once ABSPATH . 'wp-admin/includes/media.php';
|
||||
require_once ABSPATH . 'wp-admin/includes/file.php';
|
||||
require_once ABSPATH . 'wp-admin/includes/image.php';
|
||||
}
|
||||
|
||||
$attachment_id = media_sideload_image( $url, $post_id, $title, 'id' );
|
||||
if ( ! is_wp_error( $attachment_id ) ) {
|
||||
set_post_thumbnail( $post_id, $attachment_id );
|
||||
}
|
||||
}
|
||||
}
|
||||
77
includes/class-eb4tec-qr-code.php
Normal file
77
includes/class-eb4tec-qr-code.php
Normal file
|
|
@ -0,0 +1,77 @@
|
|||
<?php
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit;
|
||||
}
|
||||
|
||||
final class EB4TEC_QR_Code {
|
||||
|
||||
public function generate( string $data, int $size = 0 ): string|WP_Error {
|
||||
if ( $size <= 0 ) {
|
||||
$size = (int) get_option( 'eb4tec_qr_size', 200 );
|
||||
}
|
||||
|
||||
$hash = md5( $data );
|
||||
$uploads = wp_upload_dir();
|
||||
$dir = trailingslashit( $uploads['basedir'] ) . 'eb4tec-qr/';
|
||||
$file = $dir . $hash . '.png';
|
||||
$url = trailingslashit( $uploads['baseurl'] ) . 'eb4tec-qr/' . $hash . '.png';
|
||||
|
||||
if ( file_exists( $file ) ) {
|
||||
return $url;
|
||||
}
|
||||
|
||||
// Create directory if needed.
|
||||
if ( ! wp_mkdir_p( $dir ) ) {
|
||||
return new WP_Error( 'eb4tec_qr_dir', __( 'Could not create QR code directory.', 'eb4tec' ) );
|
||||
}
|
||||
|
||||
// Fetch from Google Chart API (no API key required for QR endpoint).
|
||||
$api_url = add_query_arg( [
|
||||
'cht' => 'qr',
|
||||
'chs' => "{$size}x{$size}",
|
||||
'chl' => rawurlencode( $data ),
|
||||
'choe' => 'UTF-8',
|
||||
], 'https://chart.googleapis.com/chart' );
|
||||
|
||||
$response = wp_remote_get( $api_url, [ 'timeout' => 15 ] );
|
||||
|
||||
if ( is_wp_error( $response ) ) {
|
||||
return $response;
|
||||
}
|
||||
|
||||
$code = wp_remote_retrieve_response_code( $response );
|
||||
if ( $code !== 200 ) {
|
||||
return new WP_Error( 'eb4tec_qr_fetch', __( 'QR code generation failed.', 'eb4tec' ), [ 'status' => $code ] );
|
||||
}
|
||||
|
||||
$body = wp_remote_retrieve_body( $response );
|
||||
// phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_file_put_contents
|
||||
if ( false === file_put_contents( $file, $body ) ) {
|
||||
return new WP_Error( 'eb4tec_qr_write', __( 'Could not save QR code image.', 'eb4tec' ) );
|
||||
}
|
||||
|
||||
return $url;
|
||||
}
|
||||
|
||||
public function get_attendee_qr_data( string $attendee_id, string $order_id, string $event_id ): string {
|
||||
return "EB4TEC:{$attendee_id}:{$order_id}:{$event_id}";
|
||||
}
|
||||
|
||||
public function render_qr_html( string $url, string $attendee_id = '', int $size = 0 ): string {
|
||||
if ( $size <= 0 ) {
|
||||
$size = (int) get_option( 'eb4tec_qr_size', 200 );
|
||||
}
|
||||
|
||||
$alt = $attendee_id
|
||||
? sprintf( __( 'Ticket QR code for attendee %s', 'eb4tec' ), esc_attr( $attendee_id ) )
|
||||
: __( 'Ticket QR code', 'eb4tec' );
|
||||
|
||||
return sprintf(
|
||||
'<img src="%s" alt="%s" width="%d" height="%d" class="eb4tec-qr-code" style="display:block;margin:8px 0;">',
|
||||
esc_url( $url ),
|
||||
esc_attr( $alt ),
|
||||
$size,
|
||||
$size
|
||||
);
|
||||
}
|
||||
}
|
||||
547
includes/class-eb4tec-settings.php
Normal file
547
includes/class-eb4tec-settings.php
Normal file
|
|
@ -0,0 +1,547 @@
|
|||
<?php
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit;
|
||||
}
|
||||
|
||||
final class EB4TEC_Settings {
|
||||
|
||||
public function __construct() {
|
||||
add_action( 'admin_menu', [ $this, 'add_admin_menu' ] );
|
||||
add_action( 'admin_post_eb4tec_save_settings', [ $this, 'handle_settings_save' ] );
|
||||
add_action( 'wp_ajax_eb4tec_validate_token', [ $this, 'ajax_validate_token' ] );
|
||||
add_action( 'wp_ajax_eb4tec_register_webhook', [ $this, 'ajax_register_webhook' ] );
|
||||
add_action( 'wp_ajax_eb4tec_delete_webhook', [ $this, 'ajax_delete_webhook' ] );
|
||||
add_action( 'admin_enqueue_scripts', [ $this, 'admin_enqueue_scripts' ] );
|
||||
add_action( 'admin_notices', [ $this, 'admin_notices' ] );
|
||||
}
|
||||
|
||||
public function add_admin_menu(): void {
|
||||
add_submenu_page(
|
||||
'edit.php?post_type=tribe_events',
|
||||
__( 'Eventbrite Sync', 'eb4tec' ),
|
||||
__( 'Eventbrite Sync', 'eb4tec' ),
|
||||
'manage_options',
|
||||
'eb4tec-settings',
|
||||
[ $this, 'render_settings_page' ]
|
||||
);
|
||||
|
||||
// Attendees submenu page is registered by EB4TEC_Attendees.
|
||||
}
|
||||
|
||||
public function render_settings_page(): void {
|
||||
if ( ! current_user_can( 'manage_options' ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$active_tab = sanitize_key( $_GET['tab'] ?? 'api' );
|
||||
$org_name = esc_html( get_option( 'eb4tec_org_name', '' ) );
|
||||
$last_sync = (int) get_option( 'eb4tec_last_sync_timestamp', 0 );
|
||||
$sync_result = get_transient( 'eb4tec_last_sync_result' );
|
||||
|
||||
echo '<div class="wrap">';
|
||||
echo '<h1>' . esc_html__( 'Eventbrite for The Events Calendar', 'eb4tec' ) . '</h1>';
|
||||
|
||||
// Sync status bar.
|
||||
if ( $last_sync ) {
|
||||
$sync_time = human_time_diff( $last_sync );
|
||||
echo '<p class="description">';
|
||||
printf(
|
||||
esc_html__( 'Last sync: %s ago', 'eb4tec' ),
|
||||
esc_html( $sync_time )
|
||||
);
|
||||
if ( is_array( $sync_result ) ) {
|
||||
printf(
|
||||
' — ' . esc_html__( '%d pulled, %d pushed, %d errors.', 'eb4tec' ),
|
||||
(int) ( $sync_result['pulled'] ?? 0 ),
|
||||
(int) ( $sync_result['pushed'] ?? 0 ),
|
||||
(int) ( $sync_result['errors'] ?? 0 )
|
||||
);
|
||||
}
|
||||
echo '</p>';
|
||||
}
|
||||
|
||||
// Sync Now button.
|
||||
echo '<form method="post" action="' . esc_url( admin_url( 'admin-post.php' ) ) . '" style="display:inline-block;margin-bottom:12px;">';
|
||||
wp_nonce_field( 'eb4tec_sync_now', '_eb4tec_sync_nonce' );
|
||||
echo '<input type="hidden" name="action" value="eb4tec_sync_now">';
|
||||
submit_button( __( 'Sync Now', 'eb4tec' ), 'secondary', 'submit', false );
|
||||
echo '</form>';
|
||||
|
||||
// Tabs.
|
||||
$tabs = [
|
||||
'api' => __( 'API & Credentials', 'eb4tec' ),
|
||||
'sync' => __( 'Sync Settings', 'eb4tec' ),
|
||||
'woocommerce' => __( 'WooCommerce', 'eb4tec' ),
|
||||
];
|
||||
|
||||
echo '<nav class="nav-tab-wrapper">';
|
||||
foreach ( $tabs as $slug => $label ) {
|
||||
$class = ( $slug === $active_tab ) ? 'nav-tab nav-tab-active' : 'nav-tab';
|
||||
$url = add_query_arg( [ 'page' => 'eb4tec-settings', 'tab' => $slug ], admin_url( 'edit.php?post_type=tribe_events' ) );
|
||||
printf( '<a href="%s" class="%s">%s</a>', esc_url( $url ), esc_attr( $class ), esc_html( $label ) );
|
||||
}
|
||||
echo '</nav>';
|
||||
|
||||
echo '<form method="post" action="' . esc_url( admin_url( 'admin-post.php' ) ) . '">';
|
||||
wp_nonce_field( 'eb4tec_save_settings', '_eb4tec_settings_nonce' );
|
||||
echo '<input type="hidden" name="action" value="eb4tec_save_settings">';
|
||||
echo '<input type="hidden" name="eb4tec_tab" value="' . esc_attr( $active_tab ) . '">';
|
||||
|
||||
echo '<table class="form-table" role="presentation"><tbody>';
|
||||
|
||||
match ( $active_tab ) {
|
||||
'api' => $this->render_tab_api( $org_name ),
|
||||
'sync' => $this->render_tab_sync(),
|
||||
'woocommerce' => $this->render_tab_woocommerce(),
|
||||
default => $this->render_tab_api( $org_name ),
|
||||
};
|
||||
|
||||
echo '</tbody></table>';
|
||||
|
||||
submit_button();
|
||||
echo '</form>';
|
||||
echo '</div>';
|
||||
}
|
||||
|
||||
private function render_tab_api( string $org_name ): void {
|
||||
$token = get_option( 'eb4tec_api_token', '' );
|
||||
$token_display = $token ? str_repeat( '•', 20 ) : '';
|
||||
$org_id = esc_attr( get_option( 'eb4tec_org_id', '' ) );
|
||||
|
||||
?>
|
||||
<tr>
|
||||
<th scope="row"><label for="eb4tec_api_token"><?php esc_html_e( 'Eventbrite API Token', 'eb4tec' ); ?></label></th>
|
||||
<td>
|
||||
<input type="password" id="eb4tec_api_token" name="eb4tec_api_token"
|
||||
class="regular-text" value="<?php echo esc_attr( $token_display ); ?>"
|
||||
placeholder="<?php esc_attr_e( 'Paste your private token here', 'eb4tec' ); ?>"
|
||||
autocomplete="new-password">
|
||||
<p class="description">
|
||||
<?php esc_html_e( 'Find your token at eventbrite.com → Account Settings → Developer Links → API Keys.', 'eb4tec' ); ?>
|
||||
</p>
|
||||
<button type="button" id="eb4tec-validate-token" class="button button-secondary" style="margin-top:6px;">
|
||||
<?php esc_html_e( 'Validate & Fetch Organization', 'eb4tec' ); ?>
|
||||
</button>
|
||||
<span id="eb4tec-validate-result" style="margin-left:8px;"></span>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row"><label for="eb4tec_org_id"><?php esc_html_e( 'Organization ID', 'eb4tec' ); ?></label></th>
|
||||
<td>
|
||||
<input type="text" id="eb4tec_org_id" name="eb4tec_org_id"
|
||||
class="regular-text" value="<?php echo $org_id; ?>">
|
||||
<?php if ( $org_name ) : ?>
|
||||
<p class="description"><?php echo esc_html( sprintf( __( 'Connected: %s', 'eb4tec' ), $org_name ) ); ?></p>
|
||||
<?php endif; ?>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row"><?php esc_html_e( 'Webhook URL', 'eb4tec' ); ?></th>
|
||||
<td>
|
||||
<code><?php echo esc_url( add_query_arg( 'eb4tec_webhook', '1', home_url( '/' ) ) ); ?></code>
|
||||
<br><br>
|
||||
<button type="button" id="eb4tec-register-webhook" class="button button-secondary">
|
||||
<?php esc_html_e( 'Register Webhook with Eventbrite', 'eb4tec' ); ?>
|
||||
</button>
|
||||
<button type="button" id="eb4tec-delete-webhook" class="button button-secondary" style="margin-left:6px;">
|
||||
<?php esc_html_e( 'Remove Webhook', 'eb4tec' ); ?>
|
||||
</button>
|
||||
<span id="eb4tec-webhook-result" style="margin-left:8px;"></span>
|
||||
<p class="description"><?php esc_html_e( 'Register this URL with Eventbrite to receive real-time event updates.', 'eb4tec' ); ?></p>
|
||||
</td>
|
||||
</tr>
|
||||
<?php
|
||||
}
|
||||
|
||||
private function render_tab_sync(): void {
|
||||
$interval = get_option( 'eb4tec_sync_interval', 'hourly' );
|
||||
$direction = get_option( 'eb4tec_sync_direction', 'both' );
|
||||
$label = get_option( 'eb4tec_wp_ticket_label', 'WordPress Purchase' );
|
||||
$visible = get_option( 'eb4tec_wp_ticket_visible', 'hidden' );
|
||||
$publish = get_option( 'eb4tec_auto_publish', '1' );
|
||||
$venues = get_option( 'eb4tec_sync_venues', '1' );
|
||||
$orgs = get_option( 'eb4tec_sync_organizers', '1' );
|
||||
|
||||
$intervals = [
|
||||
'hourly' => __( 'Hourly', 'eb4tec' ),
|
||||
'twicedaily' => __( 'Twice Daily', 'eb4tec' ),
|
||||
'daily' => __( 'Daily', 'eb4tec' ),
|
||||
];
|
||||
$directions = [
|
||||
'both' => __( 'Both directions', 'eb4tec' ),
|
||||
'eb_to_tec' => __( 'Eventbrite → WordPress only', 'eb4tec' ),
|
||||
'tec_to_eb' => __( 'WordPress → Eventbrite only', 'eb4tec' ),
|
||||
];
|
||||
?>
|
||||
<tr>
|
||||
<th scope="row"><label for="eb4tec_sync_interval"><?php esc_html_e( 'Sync Interval', 'eb4tec' ); ?></label></th>
|
||||
<td>
|
||||
<select id="eb4tec_sync_interval" name="eb4tec_sync_interval">
|
||||
<?php foreach ( $intervals as $val => $label_text ) : ?>
|
||||
<option value="<?php echo esc_attr( $val ); ?>" <?php selected( $interval, $val ); ?>>
|
||||
<?php echo esc_html( $label_text ); ?>
|
||||
</option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row"><label for="eb4tec_sync_direction"><?php esc_html_e( 'Sync Direction', 'eb4tec' ); ?></label></th>
|
||||
<td>
|
||||
<select id="eb4tec_sync_direction" name="eb4tec_sync_direction">
|
||||
<?php foreach ( $directions as $val => $label_text ) : ?>
|
||||
<option value="<?php echo esc_attr( $val ); ?>" <?php selected( $direction, $val ); ?>>
|
||||
<?php echo esc_html( $label_text ); ?>
|
||||
</option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row"><label for="eb4tec_wp_ticket_label"><?php esc_html_e( 'WordPress Ticket Label on Eventbrite', 'eb4tec' ); ?></label></th>
|
||||
<td>
|
||||
<input type="text" id="eb4tec_wp_ticket_label" name="eb4tec_wp_ticket_label"
|
||||
class="regular-text" value="<?php echo esc_attr( $label ); ?>">
|
||||
<p class="description"><?php esc_html_e( 'The name of the hidden free ticket class created on Eventbrite for WordPress purchases.', 'eb4tec' ); ?></p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row"><label for="eb4tec_wp_ticket_visible"><?php esc_html_e( 'WordPress Ticket Visibility on Eventbrite', 'eb4tec' ); ?></label></th>
|
||||
<td>
|
||||
<select id="eb4tec_wp_ticket_visible" name="eb4tec_wp_ticket_visible">
|
||||
<option value="hidden" <?php selected( $visible, 'hidden' ); ?>><?php esc_html_e( 'Hidden (recommended)', 'eb4tec' ); ?></option>
|
||||
<option value="visible" <?php selected( $visible, 'visible' ); ?>><?php esc_html_e( 'Visible', 'eb4tec' ); ?></option>
|
||||
</select>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row"><?php esc_html_e( 'Auto-publish Events', 'eb4tec' ); ?></th>
|
||||
<td>
|
||||
<label>
|
||||
<input type="checkbox" name="eb4tec_auto_publish" value="1" <?php checked( $publish, '1' ); ?>>
|
||||
<?php esc_html_e( 'Automatically publish events pushed to Eventbrite', 'eb4tec' ); ?>
|
||||
</label>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row"><?php esc_html_e( 'Sync Venues', 'eb4tec' ); ?></th>
|
||||
<td>
|
||||
<label>
|
||||
<input type="checkbox" name="eb4tec_sync_venues" value="1" <?php checked( $venues, '1' ); ?>>
|
||||
<?php esc_html_e( 'Sync venue data between Eventbrite and The Events Calendar', 'eb4tec' ); ?>
|
||||
</label>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row"><?php esc_html_e( 'Sync Organizers', 'eb4tec' ); ?></th>
|
||||
<td>
|
||||
<label>
|
||||
<input type="checkbox" name="eb4tec_sync_organizers" value="1" <?php checked( $orgs, '1' ); ?>>
|
||||
<?php esc_html_e( 'Sync organizer data between Eventbrite and The Events Calendar', 'eb4tec' ); ?>
|
||||
</label>
|
||||
</td>
|
||||
</tr>
|
||||
<?php
|
||||
}
|
||||
|
||||
private function render_tab_woocommerce(): void {
|
||||
$cat_id = (int) get_option( 'eb4tec_woo_category_id', 0 );
|
||||
$qr_size = (int) get_option( 'eb4tec_qr_size', 200 );
|
||||
$qr_in_email = get_option( 'eb4tec_qr_in_email', '1' );
|
||||
$qr_on_order = get_option( 'eb4tec_qr_on_order_page', '1' );
|
||||
$delete_data = get_option( 'eb4tec_delete_on_uninstall', '' );
|
||||
|
||||
$categories = get_terms( [
|
||||
'taxonomy' => 'product_cat',
|
||||
'hide_empty' => false,
|
||||
] );
|
||||
?>
|
||||
<tr>
|
||||
<th scope="row"><label for="eb4tec_woo_category_id"><?php esc_html_e( 'Ticket Product Category', 'eb4tec' ); ?></label></th>
|
||||
<td>
|
||||
<select id="eb4tec_woo_category_id" name="eb4tec_woo_category_id">
|
||||
<option value="0"><?php esc_html_e( '— None —', 'eb4tec' ); ?></option>
|
||||
<?php if ( ! is_wp_error( $categories ) ) : ?>
|
||||
<?php foreach ( $categories as $cat ) : ?>
|
||||
<option value="<?php echo esc_attr( $cat->term_id ); ?>" <?php selected( $cat_id, $cat->term_id ); ?>>
|
||||
<?php echo esc_html( $cat->name ); ?>
|
||||
</option>
|
||||
<?php endforeach; ?>
|
||||
<?php endif; ?>
|
||||
</select>
|
||||
<p class="description"><?php esc_html_e( 'WooCommerce products created for event tickets will be assigned to this category.', 'eb4tec' ); ?></p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row"><label for="eb4tec_qr_size"><?php esc_html_e( 'QR Code Size (px)', 'eb4tec' ); ?></label></th>
|
||||
<td>
|
||||
<input type="number" id="eb4tec_qr_size" name="eb4tec_qr_size"
|
||||
value="<?php echo esc_attr( $qr_size ); ?>" min="100" max="500" step="10" class="small-text">
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row"><?php esc_html_e( 'QR Code in Email', 'eb4tec' ); ?></th>
|
||||
<td>
|
||||
<label>
|
||||
<input type="checkbox" name="eb4tec_qr_in_email" value="1" <?php checked( $qr_in_email, '1' ); ?>>
|
||||
<?php esc_html_e( 'Include QR code in the WooCommerce order confirmation email', 'eb4tec' ); ?>
|
||||
</label>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row"><?php esc_html_e( 'QR Code on Order Page', 'eb4tec' ); ?></th>
|
||||
<td>
|
||||
<label>
|
||||
<input type="checkbox" name="eb4tec_qr_on_order_page" value="1" <?php checked( $qr_on_order, '1' ); ?>>
|
||||
<?php esc_html_e( 'Show QR code on the order confirmation / thank-you page', 'eb4tec' ); ?>
|
||||
</label>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row"><?php esc_html_e( 'Delete Data on Uninstall', 'eb4tec' ); ?></th>
|
||||
<td>
|
||||
<label>
|
||||
<input type="checkbox" name="eb4tec_delete_on_uninstall" value="1" <?php checked( $delete_data, '1' ); ?>>
|
||||
<?php esc_html_e( 'Remove all plugin settings and post meta when the plugin is deleted', 'eb4tec' ); ?>
|
||||
</label>
|
||||
</td>
|
||||
</tr>
|
||||
<?php
|
||||
}
|
||||
|
||||
public function handle_settings_save(): void {
|
||||
if ( ! current_user_can( 'manage_options' ) ) {
|
||||
wp_die( esc_html__( 'Insufficient permissions.', 'eb4tec' ) );
|
||||
}
|
||||
|
||||
check_admin_referer( 'eb4tec_save_settings', '_eb4tec_settings_nonce' );
|
||||
|
||||
$tab = sanitize_key( $_POST['eb4tec_tab'] ?? 'api' );
|
||||
|
||||
if ( 'api' === $tab ) {
|
||||
$raw_token = $_POST['eb4tec_api_token'] ?? '';
|
||||
// Only update if a real value was submitted (not the masked placeholder).
|
||||
if ( $raw_token && ! str_contains( $raw_token, '•' ) ) {
|
||||
update_option( 'eb4tec_api_token', sanitize_text_field( $raw_token ), false );
|
||||
}
|
||||
update_option( 'eb4tec_org_id', sanitize_text_field( $_POST['eb4tec_org_id'] ?? '' ) );
|
||||
}
|
||||
|
||||
if ( 'sync' === $tab ) {
|
||||
$interval = sanitize_key( $_POST['eb4tec_sync_interval'] ?? 'hourly' );
|
||||
if ( ! in_array( $interval, [ 'hourly', 'twicedaily', 'daily' ], true ) ) {
|
||||
$interval = 'hourly';
|
||||
}
|
||||
$old_interval = get_option( 'eb4tec_sync_interval', 'hourly' );
|
||||
update_option( 'eb4tec_sync_interval', $interval );
|
||||
if ( $interval !== $old_interval ) {
|
||||
EB4TEC_Cron::unschedule();
|
||||
EB4TEC_Cron::schedule();
|
||||
}
|
||||
|
||||
$direction = sanitize_key( $_POST['eb4tec_sync_direction'] ?? 'both' );
|
||||
if ( ! in_array( $direction, [ 'both', 'eb_to_tec', 'tec_to_eb' ], true ) ) {
|
||||
$direction = 'both';
|
||||
}
|
||||
update_option( 'eb4tec_sync_direction', $direction );
|
||||
update_option( 'eb4tec_wp_ticket_label', sanitize_text_field( $_POST['eb4tec_wp_ticket_label'] ?? 'WordPress Purchase' ) );
|
||||
$visible = sanitize_key( $_POST['eb4tec_wp_ticket_visible'] ?? 'hidden' );
|
||||
update_option( 'eb4tec_wp_ticket_visible', in_array( $visible, [ 'hidden', 'visible' ], true ) ? $visible : 'hidden' );
|
||||
update_option( 'eb4tec_auto_publish', isset( $_POST['eb4tec_auto_publish'] ) ? '1' : '' );
|
||||
update_option( 'eb4tec_sync_venues', isset( $_POST['eb4tec_sync_venues'] ) ? '1' : '' );
|
||||
update_option( 'eb4tec_sync_organizers', isset( $_POST['eb4tec_sync_organizers'] ) ? '1' : '' );
|
||||
}
|
||||
|
||||
if ( 'woocommerce' === $tab ) {
|
||||
update_option( 'eb4tec_woo_category_id', absint( $_POST['eb4tec_woo_category_id'] ?? 0 ) );
|
||||
$qr_size = max( 100, min( 500, (int) ( $_POST['eb4tec_qr_size'] ?? 200 ) ) );
|
||||
update_option( 'eb4tec_qr_size', $qr_size );
|
||||
update_option( 'eb4tec_qr_in_email', isset( $_POST['eb4tec_qr_in_email'] ) ? '1' : '' );
|
||||
update_option( 'eb4tec_qr_on_order_page', isset( $_POST['eb4tec_qr_on_order_page'] ) ? '1' : '' );
|
||||
update_option( 'eb4tec_delete_on_uninstall', isset( $_POST['eb4tec_delete_on_uninstall'] ) ? '1' : '' );
|
||||
}
|
||||
|
||||
$redirect = add_query_arg( [
|
||||
'page' => 'eb4tec-settings',
|
||||
'tab' => $tab,
|
||||
'updated' => '1',
|
||||
], admin_url( 'edit.php?post_type=tribe_events' ) );
|
||||
|
||||
wp_safe_redirect( $redirect );
|
||||
exit;
|
||||
}
|
||||
|
||||
public function ajax_validate_token(): void {
|
||||
check_ajax_referer( 'eb4tec_admin_ajax', 'nonce' );
|
||||
|
||||
if ( ! current_user_can( 'manage_options' ) ) {
|
||||
wp_send_json_error( [ 'message' => __( 'Insufficient permissions.', 'eb4tec' ) ] );
|
||||
}
|
||||
|
||||
$token = sanitize_text_field( $_POST['token'] ?? '' );
|
||||
if ( empty( $token ) ) {
|
||||
wp_send_json_error( [ 'message' => __( 'No token provided.', 'eb4tec' ) ] );
|
||||
}
|
||||
|
||||
// Temporarily update the token so the API client picks it up.
|
||||
$old_token = get_option( 'eb4tec_api_token', '' );
|
||||
update_option( 'eb4tec_api_token', $token, false );
|
||||
|
||||
$api = new EB4TEC_API_Client();
|
||||
$result = $api->get_user_me();
|
||||
|
||||
if ( is_wp_error( $result ) ) {
|
||||
update_option( 'eb4tec_api_token', $old_token, false );
|
||||
wp_send_json_error( [ 'message' => $result->get_error_message() ] );
|
||||
}
|
||||
|
||||
// Fetch organization ID.
|
||||
$orgs = isset( $result['organizations'] ) ? $result['organizations'] : [];
|
||||
if ( empty( $orgs ) ) {
|
||||
// Try getting org from user profile.
|
||||
$org_id = '';
|
||||
$org_name = $result['name'] ?? '';
|
||||
} else {
|
||||
$org = reset( $orgs );
|
||||
$org_id = $org['id'] ?? '';
|
||||
$org_name = $org['name'] ?? '';
|
||||
}
|
||||
|
||||
update_option( 'eb4tec_org_id', sanitize_text_field( $org_id ) );
|
||||
update_option( 'eb4tec_org_name', sanitize_text_field( $org_name ) );
|
||||
|
||||
wp_send_json_success( [
|
||||
'message' => sprintf( __( 'Connected as %s (Org: %s)', 'eb4tec' ), esc_html( $result['name'] ?? '' ), esc_html( $org_name ) ),
|
||||
'org_id' => $org_id,
|
||||
'org_name' => $org_name,
|
||||
] );
|
||||
}
|
||||
|
||||
public function ajax_register_webhook(): void {
|
||||
check_ajax_referer( 'eb4tec_admin_ajax', 'nonce' );
|
||||
|
||||
if ( ! current_user_can( 'manage_options' ) ) {
|
||||
wp_send_json_error( [ 'message' => __( 'Insufficient permissions.', 'eb4tec' ) ] );
|
||||
}
|
||||
|
||||
$secret = get_option( 'eb4tec_webhook_secret', '' );
|
||||
if ( empty( $secret ) ) {
|
||||
$secret = wp_generate_password( 32, false );
|
||||
update_option( 'eb4tec_webhook_secret', $secret );
|
||||
}
|
||||
|
||||
$endpoint_url = add_query_arg( 'eb4tec_webhook', '1', home_url( '/' ) );
|
||||
$api = new EB4TEC_API_Client();
|
||||
|
||||
$result = $api->create_webhook( [
|
||||
'webhook' => [
|
||||
'endpoint_url' => $endpoint_url,
|
||||
'actions' => 'event.created,event.published,event.updated,event.unpublished,attendee.updated',
|
||||
],
|
||||
] );
|
||||
|
||||
if ( is_wp_error( $result ) ) {
|
||||
wp_send_json_error( [ 'message' => $result->get_error_message() ] );
|
||||
}
|
||||
|
||||
update_option( 'eb4tec_webhook_id', sanitize_text_field( $result['id'] ?? '' ) );
|
||||
wp_send_json_success( [ 'message' => __( 'Webhook registered successfully.', 'eb4tec' ) ] );
|
||||
}
|
||||
|
||||
public function ajax_delete_webhook(): void {
|
||||
check_ajax_referer( 'eb4tec_admin_ajax', 'nonce' );
|
||||
|
||||
if ( ! current_user_can( 'manage_options' ) ) {
|
||||
wp_send_json_error( [ 'message' => __( 'Insufficient permissions.', 'eb4tec' ) ] );
|
||||
}
|
||||
|
||||
$webhook_id = get_option( 'eb4tec_webhook_id', '' );
|
||||
if ( empty( $webhook_id ) ) {
|
||||
wp_send_json_error( [ 'message' => __( 'No webhook ID stored.', 'eb4tec' ) ] );
|
||||
}
|
||||
|
||||
$api = new EB4TEC_API_Client();
|
||||
$result = $api->delete_webhook( $webhook_id );
|
||||
|
||||
if ( is_wp_error( $result ) ) {
|
||||
wp_send_json_error( [ 'message' => $result->get_error_message() ] );
|
||||
}
|
||||
|
||||
delete_option( 'eb4tec_webhook_id' );
|
||||
wp_send_json_success( [ 'message' => __( 'Webhook removed.', 'eb4tec' ) ] );
|
||||
}
|
||||
|
||||
public function admin_enqueue_scripts( string $hook ): void {
|
||||
$screen = get_current_screen();
|
||||
if ( ! $screen || ! str_contains( $screen->id ?? '', 'eb4tec' ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$nonce = wp_create_nonce( 'eb4tec_admin_ajax' );
|
||||
wp_add_inline_script(
|
||||
'jquery',
|
||||
"(function($){ const nonce = '{$nonce}';
|
||||
$('#eb4tec-validate-token').on('click', function(){
|
||||
var token = $('#eb4tec_api_token').val();
|
||||
$.post(ajaxurl, {action:'eb4tec_validate_token',nonce:nonce,token:token}, function(r){
|
||||
var el = $('#eb4tec-validate-result');
|
||||
if(r.success){ el.css('color','green').text(r.data.message); $('#eb4tec_org_id').val(r.data.org_id); }
|
||||
else { el.css('color','red').text(r.data.message); }
|
||||
});
|
||||
});
|
||||
$('#eb4tec-register-webhook').on('click', function(){
|
||||
$.post(ajaxurl, {action:'eb4tec_register_webhook',nonce:nonce}, function(r){
|
||||
var el = $('#eb4tec-webhook-result');
|
||||
if(r.success){ el.css('color','green').text(r.data.message); }
|
||||
else { el.css('color','red').text(r.data.message); }
|
||||
});
|
||||
});
|
||||
$('#eb4tec-delete-webhook').on('click', function(){
|
||||
$.post(ajaxurl, {action:'eb4tec_delete_webhook',nonce:nonce}, function(r){
|
||||
var el = $('#eb4tec-webhook-result');
|
||||
if(r.success){ el.css('color','green').text(r.data.message); }
|
||||
else { el.css('color','red').text(r.data.message); }
|
||||
});
|
||||
});
|
||||
})(jQuery);"
|
||||
);
|
||||
}
|
||||
|
||||
public function admin_notices(): void {
|
||||
if ( ! current_user_can( 'manage_options' ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$screen = get_current_screen();
|
||||
if ( ! $screen || ! str_contains( $screen->id ?? '', 'eb4tec' ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ( ! empty( $_GET['updated'] ) ) {
|
||||
echo '<div class="notice notice-success is-dismissible"><p>' .
|
||||
esc_html__( 'Settings saved.', 'eb4tec' ) .
|
||||
'</p></div>';
|
||||
}
|
||||
|
||||
if ( get_transient( 'eb4tec_rate_limit_warning' ) ) {
|
||||
echo '<div class="notice notice-warning is-dismissible"><p>' .
|
||||
esc_html__( 'Eventbrite API rate limit is nearly exhausted. Sync operations may be delayed.', 'eb4tec' ) .
|
||||
'</p></div>';
|
||||
}
|
||||
|
||||
// Show "Sync Now" result.
|
||||
$user_id = get_current_user_id();
|
||||
$sync_result = get_transient( "eb4tec_sync_now_result_{$user_id}" );
|
||||
if ( is_array( $sync_result ) ) {
|
||||
delete_transient( "eb4tec_sync_now_result_{$user_id}" );
|
||||
$msg = sprintf(
|
||||
__( 'Sync complete — %d pulled from Eventbrite, %d pushed to Eventbrite, %d errors.', 'eb4tec' ),
|
||||
(int) ( $sync_result['pulled'] ?? 0 ),
|
||||
(int) ( $sync_result['pushed'] ?? 0 ),
|
||||
(int) ( $sync_result['errors'] ?? 0 )
|
||||
);
|
||||
$class = ( ( $sync_result['errors'] ?? 0 ) > 0 ) ? 'notice-warning' : 'notice-success';
|
||||
echo '<div class="notice ' . esc_attr( $class ) . ' is-dismissible"><p>' . esc_html( $msg ) . '</p></div>';
|
||||
}
|
||||
}
|
||||
}
|
||||
162
includes/class-eb4tec-shortcodes.php
Normal file
162
includes/class-eb4tec-shortcodes.php
Normal file
|
|
@ -0,0 +1,162 @@
|
|||
<?php
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit;
|
||||
}
|
||||
|
||||
final class EB4TEC_Shortcodes {
|
||||
|
||||
public function __construct(
|
||||
private readonly EB4TEC_API_Client $api,
|
||||
private readonly EB4TEC_Ticket_Manager $ticket_manager,
|
||||
) {
|
||||
add_action( 'init', [ $this, 'register_shortcodes' ] );
|
||||
}
|
||||
|
||||
public function register_shortcodes(): void {
|
||||
add_shortcode( 'eb4tec_capacity', [ $this, 'capacity_shortcode' ] );
|
||||
add_shortcode( 'eb4tec_tickets', [ $this, 'tickets_shortcode' ] );
|
||||
}
|
||||
|
||||
/**
|
||||
* [eb4tec_capacity event_id="123" post_id="456"]
|
||||
* Renders remaining ticket capacity for an event.
|
||||
*/
|
||||
public function capacity_shortcode( array $atts ): string {
|
||||
$post_id = $this->resolve_post_id( $atts );
|
||||
if ( ! $post_id ) {
|
||||
return '';
|
||||
}
|
||||
|
||||
$total = (int) get_post_meta( $post_id, '_eb4tec_capacity', true );
|
||||
$available = $this->ticket_manager->get_available_capacity( $post_id );
|
||||
|
||||
if ( $total <= 0 ) {
|
||||
return '';
|
||||
}
|
||||
|
||||
if ( $available <= 0 ) {
|
||||
return '<span class="eb4tec-capacity eb4tec-capacity--sold-out">' .
|
||||
esc_html__( 'Sold out', 'eb4tec' ) .
|
||||
'</span>';
|
||||
}
|
||||
|
||||
return sprintf(
|
||||
'<span class="eb4tec-capacity">%s</span>',
|
||||
esc_html( sprintf(
|
||||
/* translators: 1: available spots, 2: total capacity */
|
||||
_n( '%1$d of %2$d spot remaining', '%1$d of %2$d spots remaining', $available, 'eb4tec' ),
|
||||
$available,
|
||||
$total
|
||||
) )
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* [eb4tec_tickets event_id="123" post_id="456"]
|
||||
* Renders a list of public Eventbrite ticket classes with a Buy button.
|
||||
*/
|
||||
public function tickets_shortcode( array $atts ): string {
|
||||
$post_id = $this->resolve_post_id( $atts );
|
||||
if ( ! $post_id ) {
|
||||
return '';
|
||||
}
|
||||
|
||||
$eb_event_id = (string) get_post_meta( $post_id, '_eb4tec_event_id', true );
|
||||
if ( ! $eb_event_id ) {
|
||||
return '';
|
||||
}
|
||||
|
||||
$result = $this->api->get_ticket_classes( $eb_event_id );
|
||||
if ( is_wp_error( $result ) ) {
|
||||
return '';
|
||||
}
|
||||
|
||||
$ticket_classes = $result['ticket_classes'] ?? [];
|
||||
$wp_label = strtolower( (string) get_option( 'eb4tec_wp_ticket_label', 'WordPress Purchase' ) );
|
||||
$product_id = (int) get_post_meta( $post_id, '_eb4tec_wp_product_id', true );
|
||||
$buy_url = $product_id ? get_permalink( $product_id ) : '';
|
||||
|
||||
// Filter out the hidden WordPress Purchase class.
|
||||
$public_classes = array_filter( $ticket_classes, function( array $tc ) use ( $wp_label ): bool {
|
||||
return strtolower( $tc['name'] ?? '' ) !== $wp_label;
|
||||
} );
|
||||
|
||||
if ( empty( $public_classes ) ) {
|
||||
return '';
|
||||
}
|
||||
|
||||
$output = '<ul class="eb4tec-ticket-classes">';
|
||||
|
||||
foreach ( $public_classes as $tc ) {
|
||||
$name = esc_html( $tc['name'] ?? '' );
|
||||
$description = esc_html( $tc['description'] ?? '' );
|
||||
$free = ! empty( $tc['free'] );
|
||||
$price = $free ? esc_html__( 'Free', 'eb4tec' ) : esc_html( $tc['cost']['display'] ?? '' );
|
||||
$sold_out = ! empty( $tc['sold_out'] );
|
||||
$hidden = ! empty( $tc['hidden'] );
|
||||
|
||||
if ( $hidden ) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$output .= '<li class="eb4tec-ticket-class">';
|
||||
$output .= '<strong class="eb4tec-ticket-name">' . $name . '</strong>';
|
||||
if ( $description ) {
|
||||
$output .= ' <span class="eb4tec-ticket-description">' . $description . '</span>';
|
||||
}
|
||||
$output .= ' <span class="eb4tec-ticket-price">' . $price . '</span>';
|
||||
|
||||
if ( $sold_out ) {
|
||||
$output .= ' <span class="eb4tec-ticket-sold-out">' . esc_html__( 'Sold out', 'eb4tec' ) . '</span>';
|
||||
}
|
||||
$output .= '</li>';
|
||||
}
|
||||
|
||||
$output .= '</ul>';
|
||||
|
||||
// Add Buy Tickets button if a WC product exists.
|
||||
if ( $buy_url ) {
|
||||
$available = $this->ticket_manager->get_available_capacity( $post_id );
|
||||
if ( $available > 0 ) {
|
||||
$output .= sprintf(
|
||||
'<a href="%s" class="button eb4tec-buy-button">%s</a>',
|
||||
esc_url( $buy_url ),
|
||||
esc_html__( 'Buy Tickets', 'eb4tec' )
|
||||
);
|
||||
} else {
|
||||
$output .= '<span class="eb4tec-sold-out-badge">' . esc_html__( 'Sold Out', 'eb4tec' ) . '</span>';
|
||||
}
|
||||
}
|
||||
|
||||
return $output;
|
||||
}
|
||||
|
||||
private function resolve_post_id( array $atts ): int {
|
||||
$atts = shortcode_atts( [
|
||||
'post_id' => 0,
|
||||
'event_id' => '',
|
||||
], $atts );
|
||||
|
||||
if ( ! empty( $atts['post_id'] ) ) {
|
||||
return (int) $atts['post_id'];
|
||||
}
|
||||
|
||||
if ( ! empty( $atts['event_id'] ) ) {
|
||||
// Look up TEC post by Eventbrite event ID.
|
||||
$query = new WP_Query( [
|
||||
'post_type' => 'tribe_events',
|
||||
'post_status' => [ 'publish', 'draft' ],
|
||||
'posts_per_page' => 1,
|
||||
'fields' => 'ids',
|
||||
'meta_query' => [ [
|
||||
'key' => '_eb4tec_event_id',
|
||||
'value' => sanitize_text_field( $atts['event_id'] ),
|
||||
] ],
|
||||
] );
|
||||
return $query->posts[0] ?? 0;
|
||||
}
|
||||
|
||||
// Default to the current event in the loop.
|
||||
return get_the_ID() ?: 0;
|
||||
}
|
||||
}
|
||||
113
includes/class-eb4tec-ticket-manager.php
Normal file
113
includes/class-eb4tec-ticket-manager.php
Normal file
|
|
@ -0,0 +1,113 @@
|
|||
<?php
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit;
|
||||
}
|
||||
|
||||
final class EB4TEC_Ticket_Manager {
|
||||
|
||||
public function __construct( private readonly EB4TEC_API_Client $api ) {}
|
||||
|
||||
/**
|
||||
* Ensure the hidden "WordPress Purchase" free ticket class exists on Eventbrite.
|
||||
* Creates it if absent; updates capacity if it has changed.
|
||||
* Returns the Eventbrite ticket class ID.
|
||||
*/
|
||||
public function ensure_wp_ticket_class( string $eb_event_id, int $post_id, int $capacity ): string|WP_Error {
|
||||
$existing_class_id = (string) get_post_meta( $post_id, '_eb4tec_wp_ticket_class_id', true );
|
||||
|
||||
if ( $existing_class_id ) {
|
||||
// Update capacity in case it changed.
|
||||
$result = $this->api->update_ticket_class( $eb_event_id, $existing_class_id, $this->build_ticket_class_body( $capacity, $eb_event_id ) );
|
||||
if ( is_wp_error( $result ) ) {
|
||||
// Stale ID — try creating a fresh one.
|
||||
delete_post_meta( $post_id, '_eb4tec_wp_ticket_class_id' );
|
||||
return $this->create_wp_ticket_class( $eb_event_id, $post_id, $capacity );
|
||||
}
|
||||
return $existing_class_id;
|
||||
}
|
||||
|
||||
return $this->create_wp_ticket_class( $eb_event_id, $post_id, $capacity );
|
||||
}
|
||||
|
||||
private function create_wp_ticket_class( string $eb_event_id, int $post_id, int $capacity ): string|WP_Error {
|
||||
$result = $this->api->create_ticket_class( $eb_event_id, $this->build_ticket_class_body( $capacity, $eb_event_id ) );
|
||||
|
||||
if ( is_wp_error( $result ) ) {
|
||||
return $result;
|
||||
}
|
||||
|
||||
$class_id = $result['id'] ?? '';
|
||||
if ( empty( $class_id ) ) {
|
||||
return new WP_Error( 'eb4tec_no_class_id', __( 'Eventbrite did not return a ticket class ID.', 'eb4tec' ) );
|
||||
}
|
||||
|
||||
update_post_meta( $post_id, '_eb4tec_wp_ticket_class_id', sanitize_text_field( $class_id ) );
|
||||
return $class_id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sync capacity to both the Eventbrite ticket class and the WooCommerce product stock.
|
||||
*/
|
||||
public function sync_capacity( int $post_id, int $new_capacity ): bool {
|
||||
update_post_meta( $post_id, '_eb4tec_capacity', $new_capacity );
|
||||
update_post_meta( $post_id, '_EventCapacity', $new_capacity );
|
||||
|
||||
// Update WC product stock.
|
||||
$product_id = (int) get_post_meta( $post_id, '_eb4tec_wp_product_id', true );
|
||||
if ( $product_id ) {
|
||||
$product = wc_get_product( $product_id );
|
||||
if ( $product ) {
|
||||
$product->set_stock_quantity( $new_capacity );
|
||||
$product->save();
|
||||
}
|
||||
}
|
||||
|
||||
// Update EB ticket class.
|
||||
$eb_event_id = (string) get_post_meta( $post_id, '_eb4tec_event_id', true );
|
||||
$class_id = (string) get_post_meta( $post_id, '_eb4tec_wp_ticket_class_id', true );
|
||||
if ( $eb_event_id && $class_id ) {
|
||||
$result = $this->api->update_ticket_class( $eb_event_id, $class_id, $this->build_ticket_class_body( $new_capacity, $eb_event_id ) );
|
||||
return ! is_wp_error( $result );
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Remaining available spots based on WooCommerce stock.
|
||||
*/
|
||||
public function get_available_capacity( int $post_id ): int {
|
||||
$product_id = (int) get_post_meta( $post_id, '_eb4tec_wp_product_id', true );
|
||||
if ( ! $product_id ) {
|
||||
return (int) get_post_meta( $post_id, '_eb4tec_capacity', true );
|
||||
}
|
||||
|
||||
$product = wc_get_product( $product_id );
|
||||
if ( ! $product || ! $product->managing_stock() ) {
|
||||
return (int) get_post_meta( $post_id, '_eb4tec_capacity', true );
|
||||
}
|
||||
|
||||
return max( 0, (int) $product->get_stock_quantity() );
|
||||
}
|
||||
|
||||
private function build_ticket_class_body( int $capacity, string $eb_event_id ): array {
|
||||
$label = (string) get_option( 'eb4tec_wp_ticket_label', 'WordPress Purchase' );
|
||||
$hidden = get_option( 'eb4tec_wp_ticket_visible', 'hidden' ) === 'hidden';
|
||||
|
||||
$body = [
|
||||
'ticket_class' => [
|
||||
'name' => $label,
|
||||
'free' => true,
|
||||
'minimum_quantity' => 1,
|
||||
'maximum_quantity' => 10,
|
||||
'hidden' => $hidden,
|
||||
],
|
||||
];
|
||||
|
||||
if ( $capacity > 0 ) {
|
||||
$body['ticket_class']['quantity_total'] = $capacity;
|
||||
}
|
||||
|
||||
return $body;
|
||||
}
|
||||
}
|
||||
125
includes/class-eb4tec-venue-sync.php
Normal file
125
includes/class-eb4tec-venue-sync.php
Normal file
|
|
@ -0,0 +1,125 @@
|
|||
<?php
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit;
|
||||
}
|
||||
|
||||
final class EB4TEC_Venue_Sync {
|
||||
|
||||
public function __construct( private readonly EB4TEC_API_Client $api ) {}
|
||||
|
||||
public function pull_venue( string $eb_venue_id ): int|WP_Error {
|
||||
if ( empty( $eb_venue_id ) ) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
$result = $this->api->get_venue( $eb_venue_id );
|
||||
if ( is_wp_error( $result ) ) {
|
||||
return $result;
|
||||
}
|
||||
|
||||
$existing_id = $this->find_tec_venue( $eb_venue_id );
|
||||
|
||||
$name = sanitize_text_field( $result['name'] ?? '' );
|
||||
$address = $result['address'] ?? [];
|
||||
|
||||
$post_args = [
|
||||
'post_title' => $name ?: __( 'Untitled Venue', 'eb4tec' ),
|
||||
'post_type' => 'tribe_venue',
|
||||
'post_status' => 'publish',
|
||||
];
|
||||
|
||||
if ( $existing_id ) {
|
||||
$post_args['ID'] = $existing_id;
|
||||
$post_id = wp_update_post( $post_args, true );
|
||||
} else {
|
||||
$post_id = wp_insert_post( $post_args, true );
|
||||
}
|
||||
|
||||
if ( is_wp_error( $post_id ) ) {
|
||||
return $post_id;
|
||||
}
|
||||
|
||||
$this->save_venue_meta( $post_id, $eb_venue_id, $address, $result );
|
||||
|
||||
return $post_id;
|
||||
}
|
||||
|
||||
public function push_venue( int $post_id ): string|WP_Error {
|
||||
$org_id = (string) get_option( 'eb4tec_org_id', '' );
|
||||
if ( empty( $org_id ) ) {
|
||||
return new WP_Error( 'eb4tec_no_org', __( 'Eventbrite organization ID not configured.', 'eb4tec' ) );
|
||||
}
|
||||
|
||||
$eb_venue_id = get_post_meta( $post_id, '_eb4tec_venue_id', true );
|
||||
$post = get_post( $post_id );
|
||||
|
||||
if ( ! $post ) {
|
||||
return new WP_Error( 'eb4tec_no_post', __( 'Venue post not found.', 'eb4tec' ) );
|
||||
}
|
||||
|
||||
$body = [
|
||||
'venue' => [
|
||||
'name' => $post->post_title,
|
||||
'address' => [
|
||||
'address_1' => (string) get_post_meta( $post_id, '_VenueAddress', true ),
|
||||
'city' => (string) get_post_meta( $post_id, '_VenueCity', true ),
|
||||
'region' => (string) get_post_meta( $post_id, '_VenueStateProvince', true ),
|
||||
'postal_code' => (string) get_post_meta( $post_id, '_VenueZip', true ),
|
||||
'country' => (string) get_post_meta( $post_id, '_VenueCountry', true ),
|
||||
],
|
||||
],
|
||||
];
|
||||
|
||||
if ( $eb_venue_id ) {
|
||||
$result = $this->api->update_venue( $eb_venue_id, $body );
|
||||
} else {
|
||||
$result = $this->api->create_venue( $org_id, $body );
|
||||
}
|
||||
|
||||
if ( is_wp_error( $result ) ) {
|
||||
return $result;
|
||||
}
|
||||
|
||||
$new_id = $result['id'] ?? '';
|
||||
if ( $new_id ) {
|
||||
update_post_meta( $post_id, '_eb4tec_venue_id', sanitize_text_field( $new_id ) );
|
||||
}
|
||||
|
||||
return $new_id ?: new WP_Error( 'eb4tec_no_venue_id', __( 'Eventbrite did not return a venue ID.', 'eb4tec' ) );
|
||||
}
|
||||
|
||||
public function find_tec_venue( string $eb_venue_id ): int {
|
||||
$query = new WP_Query( [
|
||||
'post_type' => 'tribe_venue',
|
||||
'post_status' => 'publish',
|
||||
'posts_per_page' => 1,
|
||||
'fields' => 'ids',
|
||||
'meta_query' => [ [
|
||||
'key' => '_eb4tec_venue_id',
|
||||
'value' => $eb_venue_id,
|
||||
] ],
|
||||
] );
|
||||
|
||||
return $query->posts[0] ?? 0;
|
||||
}
|
||||
|
||||
private function save_venue_meta( int $post_id, string $eb_venue_id, array $address, array $result ): void {
|
||||
update_post_meta( $post_id, '_eb4tec_venue_id', sanitize_text_field( $eb_venue_id ) );
|
||||
update_post_meta( $post_id, '_eb4tec_venue_last_synced', time() );
|
||||
|
||||
update_post_meta( $post_id, '_VenueAddress', sanitize_text_field( $address['address_1'] ?? '' ) );
|
||||
update_post_meta( $post_id, '_VenueCity', sanitize_text_field( $address['city'] ?? '' ) );
|
||||
update_post_meta( $post_id, '_VenueStateProvince', sanitize_text_field( $address['region'] ?? '' ) );
|
||||
update_post_meta( $post_id, '_VenueZip', sanitize_text_field( $address['postal_code'] ?? '' ) );
|
||||
update_post_meta( $post_id, '_VenueCountry', sanitize_text_field( $address['country'] ?? '' ) );
|
||||
|
||||
$lat = $address['latitude'] ?? '';
|
||||
$lng = $address['longitude'] ?? '';
|
||||
if ( $lat ) {
|
||||
update_post_meta( $post_id, '_VenueGeoLat', sanitize_text_field( $lat ) );
|
||||
}
|
||||
if ( $lng ) {
|
||||
update_post_meta( $post_id, '_VenueGeoLng', sanitize_text_field( $lng ) );
|
||||
}
|
||||
}
|
||||
}
|
||||
149
includes/class-eb4tec-webhook.php
Normal file
149
includes/class-eb4tec-webhook.php
Normal file
|
|
@ -0,0 +1,149 @@
|
|||
<?php
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit;
|
||||
}
|
||||
|
||||
final class EB4TEC_Webhook {
|
||||
|
||||
public function __construct(
|
||||
private readonly EB4TEC_API_Client $api,
|
||||
private readonly EB4TEC_Event_Sync $event_sync,
|
||||
) {
|
||||
add_filter( 'query_vars', [ $this, 'add_query_var' ] );
|
||||
add_action( 'template_redirect', [ $this, 'handle_webhook' ] );
|
||||
}
|
||||
|
||||
public function add_query_var( array $vars ): array {
|
||||
$vars[] = 'eb4tec_webhook';
|
||||
return $vars;
|
||||
}
|
||||
|
||||
public function handle_webhook(): void {
|
||||
if ( ! get_query_var( 'eb4tec_webhook' ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$payload = file_get_contents( 'php://input' );
|
||||
$signature = $_SERVER['HTTP_X_EVENTBRITE_SIGNATURE'] ?? '';
|
||||
|
||||
if ( ! $this->verify_signature( $payload, $signature ) ) {
|
||||
wp_die( 'Forbidden', 'Forbidden', [ 'response' => 403 ] );
|
||||
}
|
||||
|
||||
$data = json_decode( $payload, true );
|
||||
if ( ! is_array( $data ) ) {
|
||||
wp_send_json( [ 'status' => 'error', 'message' => 'Invalid JSON' ], 400 );
|
||||
}
|
||||
|
||||
$action = $data['config']['action'] ?? '';
|
||||
$api_url = $data['api_url'] ?? '';
|
||||
|
||||
// Extract EB event ID from the API URL (e.g. .../events/123456789/).
|
||||
$eb_event_id = $this->extract_event_id_from_url( $api_url );
|
||||
|
||||
switch ( $action ) {
|
||||
case 'event.published':
|
||||
case 'event.updated':
|
||||
if ( $eb_event_id ) {
|
||||
$this->event_sync->pull_event( $eb_event_id );
|
||||
}
|
||||
break;
|
||||
|
||||
case 'event.unpublished':
|
||||
if ( $eb_event_id ) {
|
||||
$this->handle_event_unpublished( $eb_event_id );
|
||||
}
|
||||
break;
|
||||
|
||||
case 'attendee.updated':
|
||||
$this->handle_attendee_updated( $data );
|
||||
break;
|
||||
|
||||
default:
|
||||
// Unknown action — acknowledge without processing.
|
||||
break;
|
||||
}
|
||||
|
||||
wp_send_json( [ 'status' => 'ok' ], 200 );
|
||||
}
|
||||
|
||||
private function verify_signature( string $payload, string $signature ): bool {
|
||||
$secret = (string) get_option( 'eb4tec_webhook_secret', '' );
|
||||
|
||||
// If no secret is configured, skip verification (allows initial setup).
|
||||
if ( empty( $secret ) ) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if ( empty( $signature ) ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$expected = hash_hmac( 'sha256', $payload, $secret );
|
||||
return hash_equals( $expected, $signature );
|
||||
}
|
||||
|
||||
private function handle_event_unpublished( string $eb_event_id ): void {
|
||||
$query = new WP_Query( [
|
||||
'post_type' => 'tribe_events',
|
||||
'post_status' => [ 'publish', 'draft' ],
|
||||
'posts_per_page' => 1,
|
||||
'fields' => 'ids',
|
||||
'meta_query' => [ [
|
||||
'key' => '_eb4tec_event_id',
|
||||
'value' => $eb_event_id,
|
||||
] ],
|
||||
] );
|
||||
|
||||
if ( ! empty( $query->posts ) ) {
|
||||
$post_id = $query->posts[0];
|
||||
wp_update_post( [
|
||||
'ID' => $post_id,
|
||||
'post_status' => 'draft',
|
||||
] );
|
||||
update_post_meta( $post_id, '_eb4tec_eb_status', 'unpublished' );
|
||||
}
|
||||
}
|
||||
|
||||
private function handle_attendee_updated( array $data ): void {
|
||||
// Extract attendee ID from the API URL if present.
|
||||
$api_url = $data['api_url'] ?? '';
|
||||
$attendee_id = $this->extract_id_from_url( $api_url, 'attendees' );
|
||||
|
||||
if ( ! $attendee_id ) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Find any WC order that has this attendee and update its note.
|
||||
global $wpdb;
|
||||
$order_ids = $wpdb->get_col(
|
||||
$wpdb->prepare(
|
||||
"SELECT post_id FROM {$wpdb->postmeta} WHERE meta_key = '_eb4tec_attendee_ids' AND meta_value LIKE %s",
|
||||
'%' . $wpdb->esc_like( $attendee_id ) . '%'
|
||||
)
|
||||
);
|
||||
|
||||
foreach ( $order_ids as $order_id ) {
|
||||
$order = wc_get_order( $order_id );
|
||||
if ( $order ) {
|
||||
$order->add_order_note(
|
||||
sprintf( __( 'EB4TEC: Eventbrite attendee %s was updated.', 'eb4tec' ), esc_html( $attendee_id ) ),
|
||||
false,
|
||||
false
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private function extract_event_id_from_url( string $url ): string {
|
||||
return $this->extract_id_from_url( $url, 'events' );
|
||||
}
|
||||
|
||||
private function extract_id_from_url( string $url, string $resource ): string {
|
||||
// URLs look like: https://www.eventbriteapi.com/v3/events/123456789/
|
||||
if ( preg_match( '#/' . preg_quote( $resource, '#' ) . '/(\d+)/#', $url, $matches ) ) {
|
||||
return $matches[1];
|
||||
}
|
||||
return '';
|
||||
}
|
||||
}
|
||||
395
includes/class-eb4tec-woocommerce.php
Normal file
395
includes/class-eb4tec-woocommerce.php
Normal file
|
|
@ -0,0 +1,395 @@
|
|||
<?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>';
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue