diff --git a/Status.md b/Status.md index a6d083b2..b00d87e3 100644 --- a/Status.md +++ b/Status.md @@ -1,6 +1,6 @@ # HVAC Community Events - Project Status -**Last Updated:** February 9, 2026 +**Last Updated:** February 20, 2026 **Version:** 2.2.18 (Deployed to Production) --- @@ -19,7 +19,39 @@ --- -## CURRENT SESSION - MARKER VISIBILITY TOGGLE CHECKBOX REFACTOR (Feb 9, 2026) +## CURRENT SESSION - SLACK NOTIFICATIONS (Feb 20, 2026) + +### Status: COMPLETE - Deployed to Production + +**Objective:** Add Slack notifications for trainer registrations, ticket purchases, and event submissions/publishes via Incoming Webhook with Block Kit rich formatting. + +### Features +- **New Trainer Registration** — name, role, organization, business type, profile photo, "View in WordPress" button +- **Ticket Purchase** — purchaser, email, event, ticket count, total, payment gateway, "View Order" button +- **Event Submitted by Trainer** — event title, trainer, date, venue, "View Event" button +- **Event Published by Admin** — same fields, "View Event" + "Edit in WP" buttons +- **Settings UI** — Webhook URL field (password type), "Send Test Notification" button with AJAX feedback +- **Test message** includes environment (Staging/Production) and site URL + +### Design Decisions +- Non-blocking `wp_remote_post` for all real notifications; blocking only for test button +- Atomic `add_post_meta(..., true)` for idempotency (prevents races and duplicate sends) +- Webhook URL validated at save-time and send-time: `https` + `hooks.slack.com` + `/services/` path +- Graceful degradation: empty webhook = disabled, missing meta = "N/A" fields, no exceptions surface to users +- Code reviewed by GPT-5 (Codex) — all 5 findings fixed before production deploy + +### Files Modified + +| File | Change | +|------|--------| +| `includes/class-hvac-slack-notifications.php` | **NEW** — static utility class with 4 notification types + test handler | +| `includes/class-hvac-settings.php` | Slack webhook URL setting + validation + test button | +| `includes/class-hvac-plugin.php` | Include file + `init()` call in `initializeSecondaryComponents()` | +| `includes/class-hvac-registration.php` | One-line call to `notify_new_registration()` after admin email | + +--- + +## PREVIOUS SESSION - MARKER VISIBILITY TOGGLE CHECKBOX REFACTOR (Feb 9, 2026) ### Status: COMPLETE - Deployed to Production (v2.2.18) diff --git a/includes/class-hvac-plugin.php b/includes/class-hvac-plugin.php index 13d558aa..e0a547e4 100644 --- a/includes/class-hvac-plugin.php +++ b/includes/class-hvac-plugin.php @@ -255,6 +255,7 @@ final class HVAC_Plugin { 'class-attendee-profile.php', 'class-hvac-page-content-fixer.php', 'class-hvac-page-content-manager.php', + 'class-hvac-slack-notifications.php', ]; // Find a Trainer feature files @@ -759,6 +760,11 @@ final class HVAC_Plugin { if (class_exists('HVAC_Announcements_Display')) { HVAC_Announcements_Display::get_instance(); } + + // Initialize Slack notifications (registration + ticket purchase hooks) + if (class_exists('HVAC_Slack_Notifications')) { + HVAC_Slack_Notifications::init(); + } error_log('HVAC Plugin: Checking if HVAC_Announcements_Admin class exists: ' . (class_exists('HVAC_Announcements_Admin') ? 'YES' : 'NO')); if (class_exists('HVAC_Announcements_Admin')) { error_log('HVAC Plugin: Instantiating HVAC_Announcements_Admin...'); diff --git a/includes/class-hvac-registration.php b/includes/class-hvac-registration.php index baf8685c..27c558d8 100644 --- a/includes/class-hvac-registration.php +++ b/includes/class-hvac-registration.php @@ -162,6 +162,11 @@ class HVAC_Registration { $this->send_admin_notification($user_id, $submitted_data); } + // Slack notification (non-blocking, fire-and-forget) + if (class_exists('HVAC_Slack_Notifications')) { + HVAC_Slack_Notifications::notify_new_registration($user_id, $submitted_data); + } + // --- Success Redirect --- $success_redirect_url = home_url('/registration-pending/'); // URL from E2E test diff --git a/includes/class-hvac-settings.php b/includes/class-hvac-settings.php index 57bd2267..6db855b4 100644 --- a/includes/class-hvac-settings.php +++ b/includes/class-hvac-settings.php @@ -63,7 +63,15 @@ class HVAC_Settings { public function register_settings() { register_setting('hvac_ce_options', 'hvac_ce_options'); - + register_setting('hvac_ce_options', 'hvac_google_maps_api_key', [ + 'type' => 'string', + 'sanitize_callback' => 'sanitize_text_field', + ]); + register_setting('hvac_ce_options', 'hvac_google_geocoding_api_key', [ + 'type' => 'string', + 'sanitize_callback' => 'sanitize_text_field', + ]); + add_settings_section( 'hvac_ce_main', __('HVAC Community Events Settings', 'hvac-ce'), @@ -78,6 +86,136 @@ class HVAC_Settings { 'hvac-ce', 'hvac_ce_main' ); + + // Google Maps API Settings Section + add_settings_section( + 'hvac_ce_google_maps', + __('Google Maps API Settings', 'hvac-ce'), + [$this, 'google_maps_section_callback'], + 'hvac-ce' + ); + + add_settings_field( + 'hvac_google_maps_api_key', + __('Maps JavaScript API Key', 'hvac-ce'), + [$this, 'maps_api_key_callback'], + 'hvac-ce', + 'hvac_ce_google_maps' + ); + + add_settings_field( + 'hvac_google_geocoding_api_key', + __('Geocoding API Key', 'hvac-ce'), + [$this, 'geocoding_api_key_callback'], + 'hvac-ce', + 'hvac_ce_google_maps' + ); + + // Slack Integration Section + register_setting('hvac_ce_options', 'hvac_slack_webhook_url', [ + 'type' => 'string', + 'sanitize_callback' => [$this, 'sanitize_slack_webhook_url'], + ]); + + add_settings_section( + 'hvac_ce_slack', + __('Slack Integration', 'hvac-ce'), + [$this, 'slack_section_callback'], + 'hvac-ce' + ); + + add_settings_field( + 'hvac_slack_webhook_url', + __('Webhook URL', 'hvac-ce'), + [$this, 'slack_webhook_url_callback'], + 'hvac-ce', + 'hvac_ce_slack' + ); + } + + /** + * Sanitize Slack webhook URL — only allow hooks.slack.com + */ + public function sanitize_slack_webhook_url(mixed $value): string { + if (!is_string($value)) { + return get_option('hvac_slack_webhook_url', ''); + } + $value = trim($value); + if ($value === '') { + return ''; + } + + $value = esc_url_raw($value); + $scheme = parse_url($value, PHP_URL_SCHEME); + $host = parse_url($value, PHP_URL_HOST); + $path = parse_url($value, PHP_URL_PATH) ?: ''; + + if ($scheme !== 'https' || $host !== 'hooks.slack.com' || !str_starts_with($path, '/services/')) { + add_settings_error( + 'hvac_slack_webhook_url', + 'invalid_url', + __('Slack webhook URL must be a valid https://hooks.slack.com/services/... URL.', 'hvac-ce'), + 'error' + ); + return get_option('hvac_slack_webhook_url', ''); // keep old value + } + + return $value; + } + + public function slack_section_callback() { + echo '

' . __('Sends notifications for new trainer registrations and ticket purchases to a Slack channel.', 'hvac-ce') . '

'; + } + + public function slack_webhook_url_callback() { + $value = get_option('hvac_slack_webhook_url', ''); + echo ''; + echo '

' . __('Create an Incoming Webhook in your Slack workspace and paste the URL here. Leave empty to disable.', 'hvac-ce') . '

'; + + if (!empty($value)) { + $nonce = wp_create_nonce('hvac_test_slack_webhook'); + echo '
'; + echo ''; + echo ''; + echo '
'; + ?> + + '; - echo '

' . + echo '

' . __('Comma-separated list of emails to notify when new trainers register', 'hvac-ce') . '

'; } + public function google_maps_section_callback() { + echo '

' . __('Configure Google Maps API keys for maps and geocoding functionality.', 'hvac-ce') . '

'; + echo '

' . __('You need two API keys: one for browser-side Maps JavaScript API (HTTP referrer restricted) and one for server-side Geocoding API (IP restricted).', 'hvac-ce') . '

'; + } + + public function maps_api_key_callback() { + $value = get_option('hvac_google_maps_api_key', ''); + echo ''; + echo '

' . __('Browser-side API key for Google Maps JavaScript API. Should be HTTP referrer restricted to your domain.', 'hvac-ce') . '

'; + } + + public function geocoding_api_key_callback() { + $value = get_option('hvac_google_geocoding_api_key', ''); + echo ''; + echo '

' . __('Server-side API key for Google Geocoding API. Should be IP restricted to your server IP address.', 'hvac-ce') . '

'; + + // Show current status + if (!empty($value)) { + echo '

✓ ' . __('Geocoding API key is configured.', 'hvac-ce') . '

'; + + // Show batch geocode button + $this->render_batch_geocode_button(); + } else { + echo '

⚠ ' . __('Geocoding API key not set. Venue addresses will not be geocoded for map display.', 'hvac-ce') . '

'; + } + } + + /** + * Render batch geocode button and status + */ + private function render_batch_geocode_button() { + // Count venues without coordinates + $venues_without_coords = get_posts([ + 'post_type' => 'tribe_venue', + 'posts_per_page' => -1, + 'post_status' => 'publish', + 'fields' => 'ids', + 'meta_query' => [ + 'relation' => 'AND', + [ + 'key' => 'venue_latitude', + 'compare' => 'NOT EXISTS' + ], + [ + 'key' => '_VenueLat', + 'compare' => 'NOT EXISTS' + ] + ] + ]); + + $count = count($venues_without_coords); + $nonce = wp_create_nonce('hvac_batch_geocode_venues'); + + echo '
'; + echo '' . __('Venue Geocoding Status', 'hvac-ce') . '
'; + + if ($count > 0) { + echo '

' . sprintf(__('%d venues need geocoding.', 'hvac-ce'), $count) . '

'; + echo ''; + echo ''; + } else { + echo '

✓ ' . __('All venues are geocoded.', 'hvac-ce') . '

'; + } + + echo '
'; + + // Render the mark as approved labs button + $this->render_mark_approved_button(); + + // Add inline JavaScript for the batch geocode button + ?> + + 'tribe_venue', + 'posts_per_page' => -1, + 'post_status' => 'publish', + 'orderby' => 'title', + 'order' => 'ASC' + ]); + + $nonce = wp_create_nonce('hvac_mark_venues_approved'); + + echo '
'; + echo '' . __('measureQuick Approved Training Labs', 'hvac-ce') . '
'; + echo '

' . __('Select which venues should appear on the Find Training map as approved training labs.', 'hvac-ce') . '

'; + + if (empty($all_venues)) { + echo '

' . __('No venues found.', 'hvac-ce') . '

'; + echo '
'; + return; + } + + // Build venue data + $venue_data = []; + $approved_count = 0; + $geocoded_count = 0; + + foreach ($all_venues as $venue) { + $is_approved = has_term('mq-approved-lab', 'venue_type', $venue->ID); + $lat = get_post_meta($venue->ID, 'venue_latitude', true) ?: get_post_meta($venue->ID, '_VenueLat', true); + $lng = get_post_meta($venue->ID, 'venue_longitude', true) ?: get_post_meta($venue->ID, '_VenueLng', true); + $has_coords = !empty($lat) && !empty($lng); + + $city = get_post_meta($venue->ID, '_VenueCity', true); + $state = get_post_meta($venue->ID, '_VenueState', true); + $location = trim($city . ($city && $state ? ', ' : '') . $state); + + if ($is_approved) $approved_count++; + if ($has_coords) $geocoded_count++; + + $venue_data[] = [ + 'id' => $venue->ID, + 'title' => $venue->post_title, + 'location' => $location, + 'is_approved' => $is_approved, + 'has_coords' => $has_coords + ]; + } + + // Summary + echo '

'; + echo sprintf(__('%d venues total, %d approved, %d geocoded', 'hvac-ce'), + count($all_venues), $approved_count, $geocoded_count); + echo '

'; + + // Scrollable list + echo '
'; + echo ''; + echo ''; + echo ''; + echo ''; + echo ''; + echo ''; + echo ''; + echo ''; + echo ''; + echo ''; + + foreach ($venue_data as $venue) { + $row_style = $venue['is_approved'] ? 'background: #e7f5e7;' : ''; + echo ''; + echo ''; + echo ''; + echo ''; + echo ''; + echo ''; + } + + echo ''; + echo '
'; + echo ''; + echo '' . __('Venue', 'hvac-ce') . '' . __('Location', 'hvac-ce') . '' . __('Geocoded', 'hvac-ce') . '
'; + echo ''; + echo '' . esc_html($venue['title']) . '' . esc_html($venue['location'] ?: '—') . ''; + echo $venue['has_coords'] ? '' : ''; + echo '
'; + echo '
'; + + // Save button + echo ''; + echo ''; + + echo ''; + + // JavaScript + ?> + + $text, + 'blocks' => $blocks, + ]); + + $response = wp_remote_post($webhook_url, [ + 'body' => $payload, + 'headers' => ['Content-Type' => 'application/json'], + 'timeout' => 5, + 'blocking' => $blocking, + ]); + + if ($blocking) { + if (is_wp_error($response)) { + error_log('[HVAC Slack] Send failed: ' . $response->get_error_message()); + return false; + } + $code = wp_remote_retrieve_response_code($response); + if ($code !== 200) { + $body = wp_remote_retrieve_body($response); + error_log("[HVAC Slack] Slack returned HTTP {$code}: {$body}"); + return false; + } + return true; + } + + // Non-blocking — optimistically assume success + if (is_wp_error($response)) { + error_log('[HVAC Slack] Non-blocking send error: ' . $response->get_error_message()); + } + + return true; + } + + // ------------------------------------------------------------------ + // Registration notification + // ------------------------------------------------------------------ + + /** + * Send a Slack notification for a new trainer registration. + * + * @param int $user_id Newly created user ID. + * @param array $submitted_data Raw form data from registration. + */ + public static function notify_new_registration(int $user_id, array $submitted_data): void { + $first_name = sanitize_text_field($submitted_data['first_name'] ?? 'N/A'); + $last_name = sanitize_text_field($submitted_data['last_name'] ?? ''); + $full_name = trim("{$first_name} {$last_name}") ?: 'N/A'; + $role = sanitize_text_field($submitted_data['role'] ?? $submitted_data['trainer_type'] ?? 'Trainer'); + $business_name = sanitize_text_field($submitted_data['business_name'] ?? $submitted_data['company'] ?? 'N/A'); + $business_type = sanitize_text_field($submitted_data['business_type'] ?? 'N/A'); + + $admin_url = admin_url("user-edit.php?user_id={$user_id}"); + $text = "New Trainer Registration: {$full_name} ({$role})"; + + // Profile image accessory (optional) + $accessory = null; + $image_id = get_user_meta($user_id, 'profile_image_id', true); + if ($image_id) { + $image_url = wp_get_attachment_image_url((int) $image_id, 'thumbnail'); + if ($image_url) { + $accessory = [ + 'type' => 'image', + 'image_url' => $image_url, + 'alt_text' => $full_name, + ]; + } + } + + $fields_section = [ + 'type' => 'section', + 'fields' => [ + ['type' => 'mrkdwn', 'text' => "*Name:*\n{$full_name}"], + ['type' => 'mrkdwn', 'text' => "*Role:*\n{$role}"], + ['type' => 'mrkdwn', 'text' => "*Organization:*\n{$business_name}"], + ['type' => 'mrkdwn', 'text' => "*Business Type:*\n{$business_type}"], + ], + ]; + + if ($accessory) { + $fields_section['accessory'] = $accessory; + } + + $blocks = [ + [ + 'type' => 'header', + 'text' => ['type' => 'plain_text', 'text' => "\xF0\x9F\x86\x95 New Trainer Registration", 'emoji' => true], + ], + $fields_section, + [ + 'type' => 'actions', + 'elements' => [ + [ + 'type' => 'button', + 'text' => ['type' => 'plain_text', 'text' => 'View in WordPress'], + 'url' => $admin_url, + 'style' => 'primary', + ], + ], + ], + ]; + + self::send($text, $blocks); + } + + // ------------------------------------------------------------------ + // Ticket purchase notification + // ------------------------------------------------------------------ + + /** + * Hook callback for transition_post_status. + * + * @param string $new_status New post status. + * @param string $old_status Previous post status. + * @param WP_Post $post Post object. + */ + public static function on_order_status_change(string $new_status, string $old_status, \WP_Post $post): void { + // Guard 1: correct post type + if ($post->post_type !== 'tec_tc_order') { + return; + } + + // Guard 2: transitioning INTO completed + if ($new_status !== 'tec-tc-completed') { + return; + } + + // Guard 3: not already completed (prevents re-saves) + if ($old_status === 'tec-tc-completed') { + return; + } + + // Atomic idempotency: add_post_meta with $unique=true prevents races + if (!add_post_meta($post->ID, '_hvac_slack_ticket_notified', '1', true)) { + return; + } + + self::notify_ticket_purchase($post->ID); + } + + /** + * Send a Slack notification for a completed ticket purchase. + * + * @param int $order_id TEC Tickets Commerce order post ID. + */ + public static function notify_ticket_purchase(int $order_id): void { + $purchaser_name = get_post_meta($order_id, '_tec_tc_order_purchaser_name', true) ?: 'N/A'; + $purchaser_email = get_post_meta($order_id, '_tec_tc_order_purchaser_email', true) ?: 'N/A'; + $order_items = get_post_meta($order_id, '_tec_tc_order_items', true); + $gateway = get_post_meta($order_id, '_tec_tc_order_gateway', true) ?: 'Unknown'; + + $total_qty = 0; + $total_price = 0.0; + $event_title = 'N/A'; + + if (is_array($order_items)) { + foreach ($order_items as $item) { + $qty = (int) ($item['quantity'] ?? 1); + $total_qty += $qty; + $total_price += (float) ($item['sub_total'] ?? $item['price'] ?? 0) * $qty; + + if (!empty($item['event_id']) && $event_title === 'N/A') { + $title = get_the_title((int) $item['event_id']); + if ($title) { + $event_title = $title; + } + } + } + } + + $total_formatted = '$' . number_format($total_price, 2); + $admin_url = admin_url("post.php?post={$order_id}&action=edit"); + $text = "New Ticket Purchase: {$purchaser_name} — {$total_qty} ticket(s) for {$event_title} ({$total_formatted})"; + + $blocks = [ + [ + 'type' => 'header', + 'text' => ['type' => 'plain_text', 'text' => "\xF0\x9F\x8E\x9F\xEF\xB8\x8F New Ticket Purchase", 'emoji' => true], + ], + [ + 'type' => 'section', + 'fields' => [ + ['type' => 'mrkdwn', 'text' => "*Purchaser:*\n{$purchaser_name}"], + ['type' => 'mrkdwn', 'text' => "*Email:*\n{$purchaser_email}"], + ['type' => 'mrkdwn', 'text' => "*Event:*\n{$event_title}"], + ['type' => 'mrkdwn', 'text' => "*Tickets:*\n{$total_qty}"], + ], + ], + [ + 'type' => 'section', + 'fields' => [ + ['type' => 'mrkdwn', 'text' => "*Total:*\n{$total_formatted}"], + ], + ], + [ + 'type' => 'context', + 'elements' => [ + ['type' => 'mrkdwn', 'text' => "via {$gateway}"], + ], + ], + [ + 'type' => 'actions', + 'elements' => [ + [ + 'type' => 'button', + 'text' => ['type' => 'plain_text', 'text' => 'View Order'], + 'url' => $admin_url, + 'style' => 'primary', + ], + ], + ], + ]; + + self::send($text, $blocks); + } + + // ------------------------------------------------------------------ + // Event notifications + // ------------------------------------------------------------------ + + /** + * Build common event Block Kit fields from an event post ID. + * + * @param int $event_id The tribe_events post ID. + * @return array{text: string, fields: array, admin_url: string, event_title: string} + */ + private static function build_event_fields(int $event_id): array { + $event = get_post($event_id); + $event_title = $event ? $event->post_title : 'N/A'; + + $start_date = get_post_meta($event_id, '_EventStartDate', true); + $end_date = get_post_meta($event_id, '_EventEndDate', true); + $venue_id = get_post_meta($event_id, '_EventVenueID', true); + $venue_name = $venue_id ? get_the_title((int) $venue_id) : 'N/A'; + + $date_display = 'N/A'; + if ($start_date) { + $date_display = wp_date('M j, Y g:ia', strtotime($start_date)); + if ($end_date) { + $date_display .= ' — ' . wp_date('g:ia', strtotime($end_date)); + } + } + + // Get trainer/author info + $author_id = $event ? (int) $event->post_author : 0; + $author_name = $author_id ? get_the_author_meta('display_name', $author_id) : 'N/A'; + + $admin_url = admin_url("post.php?post={$event_id}&action=edit"); + $front_url = get_permalink($event_id) ?: $admin_url; + + $fields = [ + ['type' => 'mrkdwn', 'text' => "*Event:*\n{$event_title}"], + ['type' => 'mrkdwn', 'text' => "*Trainer:*\n{$author_name}"], + ['type' => 'mrkdwn', 'text' => "*Date:*\n{$date_display}"], + ['type' => 'mrkdwn', 'text' => "*Venue:*\n{$venue_name}"], + ]; + + return [ + 'event_title' => $event_title, + 'author_name' => $author_name, + 'fields' => $fields, + 'admin_url' => $admin_url, + 'front_url' => $front_url, + ]; + } + + /** + * Called when a trainer submits an event via TEC Community Events form. + * Hooked to `hvac_tec_event_saved`. + * + * @param int $event_id The saved event post ID. + */ + public static function notify_event_submitted(int $event_id): void { + // Atomic lock: add_post_meta with $unique=true returns false if key already exists. + // Prevents duplicate sends on edits and concurrent requests. + if (!add_post_meta($event_id, '_hvac_slack_event_notified', '1', true)) { + return; + } + + $data = self::build_event_fields($event_id); + $text = "New Event Submitted: {$data['event_title']} by {$data['author_name']}"; + + $blocks = [ + [ + 'type' => 'header', + 'text' => ['type' => 'plain_text', 'text' => "\xF0\x9F\x93\x9D Event Submitted by Trainer", 'emoji' => true], + ], + [ + 'type' => 'section', + 'fields' => $data['fields'], + ], + [ + 'type' => 'actions', + 'elements' => [ + [ + 'type' => 'button', + 'text' => ['type' => 'plain_text', 'text' => 'View Event'], + 'url' => $data['admin_url'], + 'style' => 'primary', + ], + ], + ], + ]; + + self::send($text, $blocks); + } + + /** + * Hook callback for transition_post_status on tribe_events. + * Fires when an admin publishes an event (including from draft/pending). + * + * @param string $new_status New post status. + * @param string $old_status Previous post status. + * @param WP_Post $post Post object. + */ + public static function on_event_status_change(string $new_status, string $old_status, \WP_Post $post): void { + if ($post->post_type !== 'tribe_events') { + return; + } + + // Only notify when transitioning INTO publish + if ($new_status !== 'publish' || $old_status === 'publish') { + return; + } + + // Atomic lock: prevents double-notify from trainer submission hook or concurrent requests + if (!add_post_meta($post->ID, '_hvac_slack_event_notified', '1', true)) { + return; + } + + $data = self::build_event_fields($post->ID); + $text = "Event Published: {$data['event_title']}"; + + $blocks = [ + [ + 'type' => 'header', + 'text' => ['type' => 'plain_text', 'text' => "\xE2\x9C\x85 Event Published", 'emoji' => true], + ], + [ + 'type' => 'section', + 'fields' => $data['fields'], + ], + [ + 'type' => 'actions', + 'elements' => [ + [ + 'type' => 'button', + 'text' => ['type' => 'plain_text', 'text' => 'View Event'], + 'url' => $data['front_url'], + ], + [ + 'type' => 'button', + 'text' => ['type' => 'plain_text', 'text' => 'Edit in WP'], + 'url' => $data['admin_url'], + ], + ], + ], + ]; + + self::send($text, $blocks); + } + + // ------------------------------------------------------------------ + // Test notification (AJAX) + // ------------------------------------------------------------------ + + /** + * AJAX handler: send a test notification to verify webhook config. + * Uses blocking mode so the admin UI gets real success/failure feedback. + */ + public static function send_test_notification(): void { + if (!current_user_can('manage_options')) { + wp_send_json_error(['message' => 'Unauthorized'], 403); + } + + check_ajax_referer('hvac_test_slack_webhook', 'nonce'); + + $env = function_exists('hvac_is_staging_environment') && hvac_is_staging_environment() ? 'Staging' : 'Production'; + $site_url = home_url('/'); + + $text = "Test notification from HVAC Community Events ({$env})"; + $blocks = [ + [ + 'type' => 'header', + 'text' => ['type' => 'plain_text', 'text' => "\xE2\x9C\x85 Slack Integration Test", 'emoji' => true], + ], + [ + 'type' => 'section', + 'text' => [ + 'type' => 'mrkdwn', + 'text' => "This is a test notification from *HVAC Community Events*.\nIf you see this, your webhook is working correctly.", + ], + ], + [ + 'type' => 'section', + 'fields' => [ + ['type' => 'mrkdwn', 'text' => "*Environment:*\n{$env}"], + ['type' => 'mrkdwn', 'text' => "*Site:*\n{$site_url}"], + ], + ], + [ + 'type' => 'context', + 'elements' => [ + ['type' => 'mrkdwn', 'text' => 'Sent at ' . current_time('Y-m-d H:i:s')], + ], + ], + ]; + + $result = self::send($text, $blocks, blocking: true); + + if ($result === null) { + wp_send_json_error(['message' => 'No webhook URL configured.']); + } elseif ($result === false) { + wp_send_json_error(['message' => 'Slack returned an error. Check the webhook URL and try again.']); + } else { + wp_send_json_success(['message' => 'Test notification sent successfully!']); + } + } +}