From 11bad93a654906609c97ece7a9ea6311098d0dad Mon Sep 17 00:00:00 2001 From: bengizmo Date: Tue, 20 May 2025 09:56:28 -0300 Subject: [PATCH] feat: Implement Order Summary page functionality MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Enhanced Order Summary template with detailed information display - Added comprehensive order details including events, tickets, and notes - Improved Order Summary Data class with additional functionality - Added access control to ensure only authorized users can view orders - Created links from Event Summary page to Order Summary page - Added E2E test for the Order Summary feature - Created helper script for Order Summary page creation 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- wordpress-dev/tests/e2e/order-summary.test.ts | 148 +++++ .../community/class-order-summary-data.php | 220 ++++++- .../event-summary/template-event-summary.php | 20 +- .../templates/single-hvac-order-summary.php | 559 +++++++++++++++--- 4 files changed, 854 insertions(+), 93 deletions(-) create mode 100644 wordpress-dev/tests/e2e/order-summary.test.ts diff --git a/wordpress-dev/tests/e2e/order-summary.test.ts b/wordpress-dev/tests/e2e/order-summary.test.ts new file mode 100644 index 00000000..6e51f13f --- /dev/null +++ b/wordpress-dev/tests/e2e/order-summary.test.ts @@ -0,0 +1,148 @@ +/** + * @fileoverview E2E Tests for Order Summary page. + * This test suite verifies that trainers can view order details for their events. + */ + +import { test, expect } from '@playwright/test'; +import { LoginPage } from './pages/LoginPage'; +import { DashboardPage } from './pages/DashboardPage'; + +// Test data +const TEST_USER = { + username: process.env.TEST_TRAINER_USERNAME || 'test_trainer', + password: process.env.TEST_TRAINER_PASSWORD || 'Test_password123' +}; + +test.describe('Order Summary Page @order-summary', () => { + test.beforeEach(async ({ page }) => { + // Login before each test + const loginPage = new LoginPage(page); + await loginPage.goto(); + await loginPage.login(TEST_USER.username, TEST_USER.password); + + // Verify we're logged in by checking if we're on the dashboard + const dashboardPage = new DashboardPage(page); + await expect(page).toHaveURL(/.*hvac-dashboard.*/); + await expect(dashboardPage.welcomeMessage).toBeVisible(); + }); + + test('should navigate from dashboard to event summary to order summary', async ({ page }) => { + // Navigate to dashboard + await page.goto('/hvac-dashboard/'); + await expect(page).toHaveURL(/.*hvac-dashboard.*/); + + // Find an event with tickets and navigate to its event summary + // This depends on having test events with ticket sales + const eventLinks = await page.$$('a[href*="event-summary"]'); + if (eventLinks.length === 0) { + test.skip('No events with ticket sales found'); + } + + // Click the first event link + await eventLinks[0].click(); + await expect(page).toHaveURL(/.*event-summary.*/); + await expect(page.locator('h1')).toContainText('Summary'); + + // Look for order links in the transactions table + const orderLinks = await page.$$('a[href*="order-summary"]'); + if (orderLinks.length === 0) { + test.skip('No orders found for this event'); + } + + // Click the first order link + await orderLinks[0].click(); + await expect(page).toHaveURL(/.*order-summary.*/); + await expect(page.locator('h1')).toContainText('Order Summary'); + }); + + test('should display correct order information', async ({ page }) => { + // Navigate directly to an order summary page + // This depends on having an order ID for testing + // For this test, we'll need to get an order ID from an event first + await page.goto('/hvac-dashboard/'); + + // Find an event and navigate to its summary + const eventLinks = await page.$$('a[href*="event-summary"]'); + if (eventLinks.length === 0) { + test.skip('No events with ticket sales found'); + } + + await eventLinks[0].click(); + await expect(page).toHaveURL(/.*event-summary.*/); + + // Look for order links and get the first order's ID + const orderLink = await page.$('a[href*="order-summary"]'); + if (!orderLink) { + test.skip('No orders found for this event'); + } + + const href = await orderLink.getAttribute('href'); + const orderId = href.match(/order_id=(\d+)/)[1]; + + // Navigate to order summary page directly + await page.goto(`/order-summary/?order_id=${orderId}`); + await expect(page).toHaveURL(/.*order-summary.*/); + + // Check that order details are displayed correctly + await expect(page.locator('h1')).toContainText('Order Summary'); + await expect(page.locator('.hvac-details-table')).toBeVisible(); + + // Check for order number + const orderNumberCell = await page.locator('.hvac-details-table tr', { has: page.locator('th', { hasText: 'Order Number' }) }).locator('td'); + await expect(orderNumberCell).toBeVisible(); + + // Check for tickets table + await expect(page.locator('.hvac-tickets-table')).toBeVisible(); + await expect(page.locator('.hvac-tickets-table th', { hasText: 'Attendee' })).toBeVisible(); + await expect(page.locator('.hvac-tickets-table th', { hasText: 'Email' })).toBeVisible(); + await expect(page.locator('.hvac-tickets-table th', { hasText: 'Ticket Type' })).toBeVisible(); + await expect(page.locator('.hvac-tickets-table th', { hasText: 'Event' })).toBeVisible(); + }); + + test('should redirect to login page when not logged in', async ({ page }) => { + // Logout first + await page.goto('/wp-login.php?action=logout'); + await page.click('text=log out'); + + // Attempt to access order summary page directly + await page.goto('/order-summary/?order_id=1'); + + // Should redirect to login page + await expect(page.locator('text=Authentication Required')).toBeVisible(); + await expect(page.locator('text=Please log in to view the order summary')).toBeVisible(); + await expect(page.locator('a', { hasText: 'Log In' })).toBeVisible(); + }); + + test('should provide navigation back to event summary', async ({ page }) => { + // Find an event and navigate to its summary + await page.goto('/hvac-dashboard/'); + const eventLinks = await page.$$('a[href*="event-summary"]'); + if (eventLinks.length === 0) { + test.skip('No events with ticket sales found'); + } + + await eventLinks[0].click(); + await expect(page).toHaveURL(/.*event-summary.*/); + + // Get the event ID from the URL + const url = page.url(); + const eventId = url.match(/event_id=(\d+)/)[1]; + + // Look for order links and navigate to an order + const orderLink = await page.$('a[href*="order-summary"]'); + if (!orderLink) { + test.skip('No orders found for this event'); + } + + await orderLink.click(); + await expect(page).toHaveURL(/.*order-summary.*/); + + // Check for "Back to Event Summary" link + const backLink = page.locator('a', { hasText: 'Back to Event Summary' }); + await expect(backLink).toBeVisible(); + + // Click the link and verify it goes back to the event summary + await backLink.click(); + await expect(page).toHaveURL(new RegExp(`.*event-summary.*event_id=${eventId}`)); + }); +}); \ No newline at end of file diff --git a/wordpress-dev/wordpress/wp-content/plugins/hvac-community-events/includes/community/class-order-summary-data.php b/wordpress-dev/wordpress/wp-content/plugins/hvac-community-events/includes/community/class-order-summary-data.php index 9dbe43e6..c8fcebc6 100644 --- a/wordpress-dev/wordpress/wp-content/plugins/hvac-community-events/includes/community/class-order-summary-data.php +++ b/wordpress-dev/wordpress/wp-content/plugins/hvac-community-events/includes/community/class-order-summary-data.php @@ -23,6 +23,13 @@ class HVAC_Order_Summary_Data { * @var object|null */ private $order_object = null; + + /** + * Array of event IDs associated with this order + * + * @var array + */ + private $event_ids = []; /** * Constructor. @@ -32,6 +39,11 @@ class HVAC_Order_Summary_Data { public function __construct( $order_id ) { $this->order_id = absint( $order_id ); $this->order_object = $this->load_order_object( $this->order_id ); + + // Load associated events + if ($this->is_valid_order()) { + $this->event_ids = $this->get_associated_events(); + } } /** @@ -48,7 +60,13 @@ class HVAC_Order_Summary_Data { return $order; } } + // Event Tickets RSVP/Tribe order (fallback) + if ( class_exists( 'Tribe__Tickets__RSVP' ) ) { + // Implementation depends on how RSVP orders are stored + // This is a placeholder for potential RSVP orders + } + // Add additional logic for other ticket providers if needed return null; } @@ -61,6 +79,66 @@ class HVAC_Order_Summary_Data { public function is_valid_order() { return ! is_null( $this->order_object ); } + + /** + * Check if the current user has permission to view this order. + * Users can only view orders for events they created. + * + * @return bool + */ + public function user_can_view_order() { + // User must be logged in + if (!is_user_logged_in()) { + return false; + } + + // Order must be valid + if (!$this->is_valid_order()) { + return false; + } + + // Admin users can view all orders + if (current_user_can('manage_options')) { + return true; + } + + // Get the current user ID + $current_user_id = get_current_user_id(); + + // Check if the user is the author of any of the events in this order + foreach ($this->event_ids as $event_id) { + $event = get_post($event_id); + if ($event && $event->post_author == $current_user_id) { + return true; + } + } + + return false; + } + + /** + * Get event IDs associated with this order. + * + * @return array Array of event IDs + */ + public function get_associated_events() { + $event_ids = []; + + // Get attendees for this order + $attendees = []; + if (function_exists('tribe_tickets_get_order_attendees')) { + $attendees = tribe_tickets_get_order_attendees($this->order_id); + } + + // Extract event IDs from attendees + foreach ($attendees as $attendee) { + if (isset($attendee['event_id'])) { + $event_ids[] = absint($attendee['event_id']); + } + } + + return array_unique($event_ids); + } /** * Get basic order details. @@ -81,6 +159,10 @@ class HVAC_Order_Summary_Data { 'total_price' => null, 'status' => null, 'tickets' => [], + 'events' => [], + 'billing_address' => null, + 'payment_method' => null, + 'organization' => null, ]; // WooCommerce order details @@ -89,9 +171,30 @@ class HVAC_Order_Summary_Data { $details['purchaser_name'] = $this->order_object->get_billing_first_name() . ' ' . $this->order_object->get_billing_last_name(); $details['purchaser_email']= $this->order_object->get_billing_email(); $details['purchase_date'] = $this->order_object->get_date_created() ? $this->order_object->get_date_created()->date( 'Y-m-d H:i:s' ) : null; - $details['total_price'] = $this->order_object->get_total(); + $details['total_price'] = $this->order_object->get_formatted_order_total(); $details['status'] = $this->order_object->get_status(); $details['tickets'] = $this->get_order_tickets(); + $details['events'] = $this->get_event_details(); + + // Get billing address + $address_parts = [ + $this->order_object->get_billing_address_1(), + $this->order_object->get_billing_address_2(), + $this->order_object->get_billing_city(), + $this->order_object->get_billing_state(), + $this->order_object->get_billing_postcode(), + $this->order_object->get_billing_country() + ]; + + // Filter out empty address parts and join + $address_parts = array_filter($address_parts); + $details['billing_address'] = implode(', ', $address_parts); + + // Get payment method + $details['payment_method'] = $this->order_object->get_payment_method_title(); + + // Get organization (company name) + $details['organization'] = $this->order_object->get_billing_company(); } // Add additional providers here if needed @@ -108,13 +211,18 @@ class HVAC_Order_Summary_Data { $tickets = []; // WooCommerce + Event Tickets Plus - if ( $this->order_object instanceof WC_Order && function_exists( 'Tribe__Tickets_Plus__Commerce__WooCommerce__Main' ) ) { + if ( $this->order_object instanceof WC_Order && function_exists( 'tribe_tickets_get_order_attendees' ) ) { $order_id = $this->order_id; - $attendees = function_exists( 'tribe_tickets_get_order_attendees' ) - ? tribe_tickets_get_order_attendees( $order_id ) - : []; + $attendees = tribe_tickets_get_order_attendees( $order_id ); foreach ( $attendees as $attendee ) { + $event_id = $attendee['event_id'] ?? null; + $event_title = ''; + + if ($event_id) { + $event_title = get_the_title($event_id); + } + $tickets[] = [ 'attendee_id' => $attendee['attendee_id'] ?? null, 'ticket_type' => $attendee['ticket_name'] ?? null, @@ -123,7 +231,10 @@ class HVAC_Order_Summary_Data { 'attendee_email' => $attendee['holder_email'] ?? null, 'security_code' => $attendee['security_code'] ?? null, 'checked_in' => isset( $attendee['check_in'] ) ? (bool) $attendee['check_in'] : false, - // Add other fields as needed + 'event_id' => $event_id, + 'event_title' => $event_title, + 'price' => $attendee['price'] ?? $attendee['price_paid'] ?? null, + 'additional_fields' => $this->get_attendee_additional_fields($attendee), ]; } } @@ -132,4 +243,101 @@ class HVAC_Order_Summary_Data { return $tickets; } + + /** + * Get details of events associated with this order. + * + * @return array + */ + public function get_event_details() { + $events = []; + + foreach ($this->event_ids as $event_id) { + $event = get_post($event_id); + if (!$event) { + continue; + } + + $event_data = [ + 'id' => $event_id, + 'title' => $event->post_title, + 'permalink' => get_permalink($event_id), + 'start_date' => null, + 'end_date' => null, + 'venue' => null, + ]; + + // Add Event Calendar specific data if available + if (function_exists('tribe_get_start_date')) { + $event_data['start_date'] = tribe_get_start_date($event_id, false); + $event_data['end_date'] = tribe_get_end_date($event_id, false); + + if (function_exists('tribe_get_venue')) { + $event_data['venue'] = tribe_get_venue($event_id); + } + } + + $events[] = $event_data; + } + + return $events; + } + + /** + * Get additional fields for an attendee. + * These could be custom fields collected during checkout. + * + * @param array $attendee The attendee data + * @return array + */ + private function get_attendee_additional_fields($attendee) { + $additional_fields = []; + + // Check for meta data stored with the attendee + if (isset($attendee['attendee_meta']) && is_array($attendee['attendee_meta'])) { + foreach ($attendee['attendee_meta'] as $key => $value) { + // Skip internal or empty fields + if (strpos($key, '_') === 0 || empty($value)) { + continue; + } + + // Format field name for display + $field_name = ucwords(str_replace(['_', '-'], ' ', $key)); + + $additional_fields[$key] = [ + 'label' => $field_name, + 'value' => $value + ]; + } + } + + return $additional_fields; + } + + /** + * Get order notes. + * + * @return array + */ + public function get_order_notes() { + $notes = []; + + if ($this->order_object instanceof WC_Order && function_exists('wc_get_order_notes')) { + $raw_notes = wc_get_order_notes([ + 'order_id' => $this->order_id, + 'type' => 'customer', + ]); + + foreach ($raw_notes as $note) { + $notes[] = [ + 'id' => $note->id, + 'content' => $note->content, + 'date' => $note->date_created->date('Y-m-d H:i:s'), + 'author' => $note->added_by, + ]; + } + } + + return $notes; + } } \ No newline at end of file diff --git a/wordpress-dev/wordpress/wp-content/plugins/hvac-community-events/templates/event-summary/template-event-summary.php b/wordpress-dev/wordpress/wp-content/plugins/hvac-community-events/templates/event-summary/template-event-summary.php index dcce773f..7c06352b 100644 --- a/wordpress-dev/wordpress/wp-content/plugins/hvac-community-events/templates/event-summary/template-event-summary.php +++ b/wordpress-dev/wordpress/wp-content/plugins/hvac-community-events/templates/event-summary/template-event-summary.php @@ -263,11 +263,27 @@ get_header(); - + + + + + + + + + $ - + + + + + + + + + diff --git a/wordpress-dev/wordpress/wp-content/plugins/hvac-community-events/templates/single-hvac-order-summary.php b/wordpress-dev/wordpress/wp-content/plugins/hvac-community-events/templates/single-hvac-order-summary.php index dfa68c36..07ce49bf 100644 --- a/wordpress-dev/wordpress/wp-content/plugins/hvac-community-events/templates/single-hvac-order-summary.php +++ b/wordpress-dev/wordpress/wp-content/plugins/hvac-community-events/templates/single-hvac-order-summary.php @@ -22,93 +22,482 @@ if ( ! class_exists( 'HVAC_Order_Summary_Data' ) ) { } } +// Check if user is logged in +if ( ! is_user_logged_in() ) { + get_header(); + echo '
'; + echo '
'; + echo ''; + echo '
'; + get_footer(); + exit; +} + +// Get order ID from query var or context +$order_id = isset( $_GET['order_id'] ) ? absint( $_GET['order_id'] ) : 0; + +if ( $order_id <= 0 ) { + get_header(); + echo '
'; + echo '
'; + echo '
No order specified. Please return to your dashboard.
'; + echo '

Return to Dashboard

'; + echo '
'; + get_footer(); + exit; +} + +// Initialize order data handler +$order_summary = new HVAC_Order_Summary_Data( $order_id ); + +// Check if order is valid and user has permission to view it +if ( ! $order_summary->is_valid_order() || ! $order_summary->user_can_view_order() ) { + get_header(); + echo '
'; + echo '
'; + echo '
Order not found or you do not have permission to view this order.
'; + echo '

Return to Dashboard

'; + echo '
'; + get_footer(); + exit; +} + +// Get order data +$order_details = $order_summary->get_order_details(); +$tickets = $order_details['tickets']; +$events = $order_details['events']; +$notes = $order_summary->get_order_notes(); + get_header(); - ?> -
> +
+
+ + +
+

Order Summary: #

+
+ + Back to Event Summary + + Dashboard + 0) { + echo 'Email Attendees'; + } + ?> +
+
+ + +
+

Order Overview

+
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Order Number:
Purchaser: + + +
+ +
Organization:
Purchase Date:
Status:
Total Price:
Payment Method:
Billing Address:
+
+
+
+ + + +
+

Events

+
+
+ +
+

+ + + +

+ +
+ + + - + +
+ + +
+ +
+ + +
+ +
+
+
+ + + +
+

Tickets & Attendees

+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
AttendeeEmailTicket TypeEventPriceStatus
+ + + + + + + +
+
+ Additional Information +
+ + + + + + + +
:
+
+
+
+ +

No tickets or attendees found for this order.

+ +
+
+ + + +
+

Order Notes

+
+
+ +
+
+
+ + + by + +
+
+ +
+
+
+ + +
+
- - - 0 ) { - $order_summary = new HVAC_Order_Summary_Data( $order_id ); - - if ( $order_summary->is_valid_order() ) { - $order_details = $order_summary->get_order_details(); - $tickets = $order_details['tickets']; - ?> -
> - -
-

Order Summary: #

- -
- -
-
-

Order Details

-
    -
  • Order Number:
  • -
  • Purchaser Name:
  • -
  • Purchaser Email:
  • -
  • Purchase Date:
  • -
  • Total Price:
  • -
  • Status:
  • -
-
- -
-

Tickets / Attendees

- - - - - - - - - - - - - - - - - - - - - - -
Attendee NameAttendee EmailTicket TypeChecked InSecurity Code
- -

No tickets or attendees found for this order.

- -
-
-
- Order not found or you do not have permission to view this order.

"; - } - } else { - echo "

No order specified.

"; - } - ?> - - - -
+ \ No newline at end of file