Log ticket purchases via usermeta on user profile
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
https://github.com/user-attachments/assets/ed8ea40b-9e1f-435f-bac8-9ecd6132884a
Screenshots
How to test the changes in this Pull Request:
While sandboxed
- Apply changes to
wp-content/plugins/camptix/camptix.php - apply changes to
wporg-profiles/class-wporg-profiles.php - apply changes to
single/profile.php - Go to https://testing.wordcamp.org/2019/tickets/ and buy tickets
- 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; ?>
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.
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
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:
- Use a horizontally scrolling table on mobile.
- 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