Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Log ticket purchases via usermeta on user profile #1418

Open
wants to merge 4 commits into
base: production
Choose a base branch
from

Conversation

renintw
Copy link
Contributor

@renintw renintw commented Oct 28, 2024

This PR tries to update the usermeta upon ticket purchase completion, enabling the purchased tickets to be displayed on the user profile. It also updates the display to reflect the corresponding status after a refund.

Currently, during a transfer (changing the name via Edit), there is no field to specify the transferee's .org account, so after the transfer, the ticket still appears in the purchaser's profile.

And if multiple tickets are purchased at once, though they are displayed in separate rows on the profile, pressing refund on one of these tickets will refund all tickets purchased in that transaction.

For the mobile UI, switching to a card-style layout could improve the design.

Any thoughts or suggestions?

See #1411

Screencasts

Screen.Capture.on.2024-10-29.at.03-14-03.mov

Screenshots

Screenshot 2024-11-01 at 14 51 10 Screenshot 2024-11-01 at 14 51 50

How to test the changes in this Pull Request:

While sandboxed

  1. Apply changes to wp-content/plugins/camptix/camptix.php
  2. apply changes to wporg-profiles/class-wporg-profiles.php
  3. apply changes to single/profile.php
  4. Go to https://testing.wordcamp.org/2019/tickets/ and buy tickets
  5. Check out https://profiles.wordpress.org/{user_name}/#content-events and try the actions.
wp-content/plugins/camptix/camptix.php
Index: wp-content/plugins/camptix/camptix.php
===================================================================
--- wp-content/plugins/camptix/camptix.php	(revision 4963)
+++ wp-content/plugins/camptix/camptix.php	(working copy)
@@ -7675,6 +7675,7 @@
 			if ( self::PAYMENT_STATUS_COMPLETED == $result ) {
 				$attendee->post_status = 'publish';
 				wp_update_post( $attendee );
+				$this->log_ticket_purchase_on_user_profile( $attendee );
 			}
 
 			if ( self::PAYMENT_STATUS_PENDING == $result ) {
@@ -7687,6 +7688,7 @@
 				wp_update_post( $attendee );
 				update_post_meta( $attendee->ID, 'tix_refund_transaction_id', $refund_transaction_id );
 				update_post_meta( $attendee->ID, 'tix_refund_transaction_details', $refund_transaction_details );
+				$this->update_ticket_status_on_user_profile( $attendee->ID, $attendee->post_status );
 				$this->log( sprintf( 'Refunded %s by user request in %s.', $transaction_id, $refund_transaction_id ), $attendee->ID, $data, 'refund' );
 			}
 
@@ -8595,6 +8597,83 @@
 	public function has_tickets_available() {
 		return $this->number_available_tickets() > 0;
 	}
+
+	/**
+	 * Log the purchased ticket information on the user's profile.
+	 *
+	 * @param object $attendee The attendee object containing ticket information.
+	 */
+	function log_ticket_purchase_on_user_profile( $attendee ) {
+		$user_id = get_current_user_id();
+		$purchase_history = get_user_meta( $user_id, 'wordcamp_ticket_history', true );
+
+		// Initialize the purchase history as an empty array if it's not already an array.
+		if ( ! is_array( $purchase_history ) ) {
+			$purchase_history = array();
+		}
+
+		// Check if the current attendee's ID is already in the purchase history.
+		$existing_attendee_ids = array_column( $purchase_history, 'id' );
+		if ( ! in_array( $attendee->ID, $existing_attendee_ids ) ) {
+			// Gather ticket details to log the purchase.
+			$ticket_id = intval( get_post_meta( $attendee->ID, 'tix_ticket_id', true ) );
+			$ticket = get_post( $ticket_id );
+			$ticket_type = $ticket->post_title;
+			$ticket_price = $this->append_currency( (float) get_post_meta( $attendee->ID, 'tix_ticket_price', true ), false );
+			$purchase_date = $attendee->post_date;
+			$edit_token = get_post_meta( $attendee->ID, 'tix_edit_token', true );
+			$edit_link = $this->get_edit_attendee_link( $attendee->ID, $edit_token );
+			$access_token = get_post_meta( $attendee->ID, 'tix_access_token', true );
+			$access_link = $this->get_access_tickets_link( $access_token );
+			$ticket_status = $attendee->post_status;
+
+			// Create a new purchase entry.
+			$new_purchase = array(
+				'id' => $attendee->ID,
+				'wordcamp_name' => get_wordcamp_name(),
+				'site_url' => site_url(),
+				'purchase_date' => $purchase_date,
+				'ticket_type' => $ticket_type,
+				'ticket_price' => $ticket_price,
+				'access_link' => $access_link,
+				'edit_link' => $edit_link,
+				'ticket_status' => $ticket_status,
+			);
+
+			// Add a refund link if the ticket is refundable.
+			if ( $this->is_refundable( $attendee->ID ) ) {
+				$new_purchase['refund_link'] = esc_url( $this->get_refund_tickets_link( $access_token ) );
+			}
+
+			$purchase_history[] = $new_purchase;
+
+			update_user_meta( $user_id, 'wordcamp_ticket_history', $purchase_history );
+		}
+	}
+
+	/**
+	 * Update purchased ticket status on the user's profile.
+	 *
+	 * @param int $attendee_id The ID of the rufunded attendee.
+	 * @param string $ticket_status Ticket status.
+	 */
+	function update_ticket_status_on_user_profile( $attendee_id, $ticket_status ) {
+		$user_id = get_current_user_id();
+		$purchase_history = get_user_meta( $user_id, 'wordcamp_ticket_history', true );
+
+		// If there's no purchase history or it's not an array, nothing to udpate.
+		if ( ! is_array( $purchase_history ) || empty( $purchase_history ) ) {
+			return;
+		}
+
+		foreach ( $purchase_history as $index => $ticket ) {
+			if ( isset( $ticket['id'] ) && intval( $ticket['id'] ) === intval( $attendee_id ) ) {
+				$purchase_history[ $index ]['ticket_status'] = $ticket_status;
+				update_user_meta( $user_id, 'wordcamp_ticket_history', $purchase_history );
+				return;
+			}
+		}
+	}
 }
 
 // Initialize the $camptix global.

wporg-profiles/class-wporg-profiles.php
Index: wporg-profiles/class-wporg-profiles.php
===================================================================
--- wporg-profiles/class-wporg-profiles.php	(revision 22903)
+++ wporg-profiles/class-wporg-profiles.php	(working copy)
@@ -70,7 +70,7 @@
 		$skip_components = array_flip( array( 'xprofile' ) );
 
 		// Types to skip.
-		$skip_types = array_flip( array( 'wordcamp_attendee_add' ) );
+		$skip_types = array_flip( array() );
 
 		$activities = $wpdb->get_results( $wpdb->prepare(
 			"SELECT *
@@ -1048,9 +1048,28 @@
 	 * @return array
 	 */
 	public function get_events() {
-		/* todo - implement this */
+		clean_user_cache( $this->user_id );
+		
+		$wordcamp_ticket_history = get_user_meta( $this->user_id, 'wordcamp_ticket_history', true );
 
-		return array( 'items' => array() );
+		// Sort ASC by purchase date.
+		usort( $wordcamp_ticket_history, function ($a, $b) { return strcmp( $b['purchase_date'], $a['purchase_date'] ); } );
+
+		// Update the 'ticket_status' to the user-friendly label.
+		$status_mapping = array(
+			'publish'  => 'Confirmed',
+			'refund' => 'Refunded',
+			'pending'  => 'Pending Confirmation',
+			'cancel' => 'Cancelled',
+			'failed' => 'Payment Failed',
+		);
+		foreach ( $wordcamp_ticket_history as &$ticket ) {
+			if ( isset( $ticket['ticket_status'] ) ) {
+				$ticket['ticket_status'] = $status_mapping[ $ticket['ticket_status'] ];
+			}
+		}
+
+		return array( 'items' => $wordcamp_ticket_history );
 	}
 
 	/**
single/profile.php
Index: single/profile.php
===================================================================
--- single/profile.php	(revision 22903)
+++ single/profile.php	(working copy)
@@ -216,7 +216,7 @@
 							</li>
 						<?php endif; ?>
 
-						<?php if ( count( $events['items'] ) ) : ?>
+						<?php if ( is_array( $events['items'] ) && count( $events['items'] ) ) : ?>
 							<li class="<?php echo 'events' == $active_class ? ' active' : 'inactive'; ?>">
 								<a href="#content-events">Events</a>
 							</li>
@@ -623,21 +623,47 @@
 						</div>
 					<?php } ?>
 
-					<?php if ( count( $events['items'] ) ) : ?>
+					<?php if ( is_array( $events['items'] ) && count( $events['items'] ) ) : ?>
 						<div id="content-events" class="info-group <?php echo 'events' == $active_class ? ' active' : 'inactive'; ?>">
-							<h4><?php echo bp_word_or_name( __( "My Events", 'buddypress' ), __( "%s's Events", 'buddypress' ), true, false ) ?></h4>
-
 							<ul>
-								<?php foreach ( $events['items'] as $event ) { ?>
-									<li>
-										<h3>
-											<a href="<?php echo esc_url( 'event url' ); ?>"><?php echo esc_html( 'event name' ); ?></a>
-										</h3>
-
-										<p>Event Description</p>
-									</li>
-
-								<?php } ?>
+								<li>
+									<table style="width:100%; border-collapse: collapse;">
+									<thead>
+										<tr style="background-color: #222; color: white; font-size: 14px;">
+										<th style="padding: 10px; border: 1px solid #444;">Event</th>
+										<th style="padding: 10px; border: 1px solid #444;">Purchase Date</th>
+										<th style="padding: 10px; border: 1px solid #444;">Ticket Type</th>
+										<th style="padding: 10px; border: 1px solid #444;">Total</th>
+										<th style="padding: 10px; border: 1px solid #444;">Actions</th>
+										<th style="padding: 10px; border: 1px solid #444;">Status</th>
+										</tr>
+									</thead>
+									<?php foreach ( $events['items'] as $ticket ) { ?>
+										<tbody>
+											<tr style="font-size: 14px;">
+											<td style="padding: 10px; border: 1px solid #444;">
+												<a href="<?php echo $ticket['site_url']; ?>"><?php echo $ticket['wordcamp_name'] ?></a>
+											</td>
+											<td style="padding: 10px; border: 1px solid #444;"><?php echo esc_html( mysql2date( get_option( 'date_format' ), $ticket['purchase_date'] ) ); ?>
+											<th style="padding: 10px; border: 1px solid #444;"><?php echo $ticket['ticket_type'] ?></th>
+											<td style="padding: 10px; border: 1px solid #444; text-wrap: nowrap;"><?php echo $ticket['ticket_price'] ?></td>
+											<td style="padding: 10px; border: 1px solid #444; text-wrap: nowrap; text-align: center;">
+												<?php if ( 'Confirmed' === $ticket['ticket_status']) { ?>
+													<a href="<?php echo esc_url( $ticket['access_link'] ); ?>">View</a>
+													/ <a href="<?php echo esc_url( $ticket['edit_link'] ); ?>">Edit</a>
+													<?php if ( ! empty( $ticket['refund_link'] ) ) { ?>
+														/ <a href="<?php echo esc_url( $ticket['refund_link'] ); ?>">Refund</a>
+													<?php } ?>
+												<?php } else { ?>
+													-
+												<?php } ?>
+											</td>
+											<td style="padding: 10px; border: 1px solid #444; text-wrap: nowrap;"><?php echo $ticket['ticket_status'] ?></td>
+											</tr>
+										</tbody>
+									<?php } ?>	
+									</table>
+								</li>
 							</ul>
 						</div>
 					<?php endif; ?>

@renintw renintw self-assigned this Oct 28, 2024
@renintw renintw linked an issue Oct 28, 2024 that may be closed by this pull request
@renintw renintw requested a review from pkevan October 28, 2024 18:50
*/
function log_ticket_purchase_on_user_profile( $attendee ) {
$user_id = get_current_user_id();
$purchase_history = get_user_meta( $user_id, 'wordcamp_ticket_history', true );
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should this be a single user meta or can it be multiple?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Using a single entry works for how I currently store the data. But yeah I should save it in a way that allows multiple access as it would make the structure and data access clearer.

Comment on lines +8619 to +8628
$ticket_id = intval( get_post_meta( $attendee->ID, 'tix_ticket_id', true ) );
$ticket = get_post( $ticket_id );
$ticket_type = $ticket->post_title;
$ticket_price = $this->append_currency( (float) get_post_meta( $attendee->ID, 'tix_ticket_price', true ), false );
$purchase_date = $attendee->post_date;
$edit_token = get_post_meta( $attendee->ID, 'tix_edit_token', true );
$edit_link = $this->get_edit_attendee_link( $attendee->ID, $edit_token );
$access_token = get_post_meta( $attendee->ID, 'tix_access_token', true );
$access_link = $this->get_access_tickets_link( $access_token );
$ticket_status = $attendee->post_status;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

are all these values needed to be stored in usermeta, or could it link to a purchase on the individual site?

return;
}

foreach ( $purchase_history as $index => $ticket ) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

by storing as a large single meta, this type of loop in then necessary, which appears difficult to understand 😄

@renintw
Copy link
Contributor Author

renintw commented Nov 1, 2024

Currently, the ticket-related data is stored in usermeta after ticket purchase completion, and then fetched directly from usermeta on the profile.

wporg-profiles/class-wporg-profiles.ph

$wordcamp_ticket_history = get_user_meta( $this->user_id, 'wordcamp_ticket_history' );

Alternatively, should we instead only save the user_id and attendee_id after purchase completion, then handle the data fetching and displaying on the Profile (similar to how activity is handled)? This approach could involve creating a new one-to-many ticket_purchases table, storing each purchase as a separate record associated with the user.

Or stroing only the WordCamp ID and attendee ID in usermeta, then accessing the data on the profile to retrieve and render the tickets the user purchased for different WordCamps.

Would this be more efficient in the long term?

Using user_meta for Each Ticket
Pros:
Simple to implement with existing WordPress functions (get_user_meta, update_user_meta).
Easy integration within WordPress, especially for small datasets.

Cons:
Limited flexibility in querying and indexing; not ideal for reporting across many users.
Others?

Using a Custom One-to-Many Table
Pros:
Easier to maintain data integrity and manage complex filtering.

Cons:
Requires additional code to set up and manage the custom table.

@renintw
Copy link
Contributor Author

renintw commented Nov 1, 2024

Regarding the UI, it currently uses a table layout for desktop. For mobile, I'm thinking an accordion style might work well, showing only the event name initially, with more details revealed when the section is expanded.

Any other suggestions? cc @WordPress/meta-design

@jasmussen
Copy link

Responsive tables are hard. If someone else chimes in with more time to help, I think a good design iteration could be spent making this shine. But as far as small fixes to get this PR going, I would do one of two things:

  1. Use a horizontally scrolling table on mobile.
  2. Find a design that works across desktop and mobile, and is the same.

For 2, that might be accordions, or it might just be listing things out. The table may be useful, I hhave not organized enough wordcamps to know this, but with just 4 items, you might just list them out in bullets.

WordCamp Testing 2019

  • Purchase date: Oct 29, 2024
  • Ticket type: General admission
  • Price: $1.00
  • Actions: —
  • Status: Refunded

WordCamp Testing 2019

  • Purchase date: Oct 29, 2024
  • Ticket type: General admission
  • Price: $1.00
  • Actions: View / Edit / Refund
  • Status: Refunded

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Log ticket purchases via usermeta
3 participants