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

Prevent duplicated orders and attendees as a result #3470

Closed
Closed
4 changes: 4 additions & 0 deletions changelog/fix-duplicated-orders-and-attendees
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
Significance: minor
Type: fix

Prevent duplicate orders and as a result duplicated attendees when a payment would initially fail at least once. [ET-2279]
2 changes: 1 addition & 1 deletion src/Tickets/Commerce/Abstract_Order.php
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,7 @@ public function get_purchaser_data( $data ) {
if ( is_user_logged_in() ) {
$user = wp_get_current_user();
$purchaser['purchaser_user_id'] = $user->ID;
$purchaser['purchaser_full_name'] = $user->first_name . ' ' . $user->last_name;
$purchaser['purchaser_full_name'] = trim( $user->first_name . ' ' . $user->last_name );
Camwyn marked this conversation as resolved.
Show resolved Hide resolved
$purchaser['purchaser_first_name'] = $user->first_name;
$purchaser['purchaser_last_name'] = $user->last_name;
$purchaser['purchaser_email'] = $user->user_email;
Expand Down
122 changes: 121 additions & 1 deletion src/Tickets/Commerce/Attendee.php
Original file line number Diff line number Diff line change
Expand Up @@ -297,7 +297,7 @@ public function delete( $attendee_id, $force = true ) {
if ( ! $attendee_id ) {
return false;
}

$event_id = (int) get_post_meta( $attendee_id, static::$event_relation_meta_key, true );

/**
Expand Down Expand Up @@ -441,6 +441,126 @@ public function create( \WP_Post $order, $ticket, array $args = [] ) {
return apply_filters( 'tec_tickets_commerce_attendee_create', $attendee, $order, $ticket, $args );
}

/**
* Updates or creates an individual attendee given an Order and Ticket.
*
* @since TBD
*
* @param \WP_Post $order Which order generated this attendee.
* @param Ticket_Object $ticket Which ticket generated this Attendee.
* @param array $args Set of extra arguments used to populate the data for the attendee.
* @param ?int $existing The ID of the existing attendee.
*
* @return \WP_Error|\WP_Post
*/
public function upsert( \WP_Post $order, $ticket, array $args = [], ?int $existing = null ) {
if ( ! $existing ) {
return $this->create( $order, $ticket, $args );
}

$update_args = [
'order_id' => $order->ID,
'ticket_id' => $ticket->ID,
'event_id' => $ticket->get_event_id(),
'security_code' => Arr::get( $args, 'security_code' ),
'opt_out' => Arr::get( $args, 'opt_out' ),
'price_paid' => Arr::get( $args, 'price_paid' ),
'currency' => Arr::get( $args, 'currency' ),
];

if ( ! empty( $order->purchaser['user_id'] ) ) {
$update_args['user_id'] = $order->purchaser['user_id'];
}

if ( ! empty( $args['email'] ) ) {
$update_args['email'] = $args['email'];
}

if (
empty( $args['email'] )
&& ! empty( $order->purchaser['email'] )
) {
$update_args['email'] = $order->purchaser['email'];
}
Comment on lines +475 to +484
Copy link
Member

Choose a reason for hiding this comment

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

Should this be an elseif?
i.e.
if ( ! empty( $args['email'] ) ) { ... }
if ( empty( $args['email'] ) && ...

Copy link
Member

Choose a reason for hiding this comment

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

root question - do we want to do both in order, or just one?


if ( ! empty( $args['full_name'] ) ) {
$update_args['full_name'] = $args['full_name'];
$update_args['title'] = $args['full_name'];
}

if (
empty( $args['full_name'] )
&& ! empty( $order->purchaser['full_name'] )
) {
Comment on lines +486 to +494
Copy link
Member

Choose a reason for hiding this comment

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

Another potential elseif.

$update_args['full_name'] = $order->purchaser['full_name'];
$update_args['title'] = $order->purchaser['full_name'];
}

$fields = Arr::get( $args, 'fields', [] );
if ( ! empty( $fields ) ) {
$update_args['fields'] = $fields;
}

// No need to update the security code.
unset( $update_args['security_code'] );

/**
* Allow the filtering of the update arguments for attendee.
*
* @since TBD
*
* @param array $update_args Which arguments we are going to use to update the attendee.
* @param \WP_Post $order Which order generated this attendee.
* @param Ticket_Object $ticket Which ticket generated this Attendee.
* @param array $args Set of extra arguments used to populate the data for the attendee.
*/
$update_args = apply_filters( 'tec_tickets_commerce_attendee_update_args', $update_args, $order, $ticket, $args );

/**
* Allow the actions before updating the attendee.
*
* @since TBD
*
* @param array $update_args Which arguments we are going to use to update the attendee.
* @param \WP_Post $order Which order generated this attendee.
* @param Ticket_Object $ticket Which ticket generated this Attendee.
* @param array $args Set of extra arguments used to populate the data for the attendee.
*/
do_action( 'tec_tickets_commerce_attendee_before_update', $update_args, $order, $ticket, $args );

$updated = tec_tc_attendees()->where( 'id', $existing )->set_args( $update_args )->save();

if ( empty( $updated[ $existing ] ) ) {
return $this->create( $order, $ticket, $args );
}

$attendee = tec_tc_attendees()->where( 'id', $existing )->first();

/**
* Allow the actions after updating the attendee.
*
* @since TBD
*
* @param \WP_Post $attendee Post object for the attendee.
* @param \WP_Post $order Which order generated this attendee.
* @param Ticket_Object $ticket Which ticket generated this Attendee.
* @param array $args Set of extra arguments used to populate the data for the attendee.
*/
do_action( 'tec_tickets_commerce_attendee_after_update', $attendee, $order, $ticket, $args );

/**
* Allow the filtering of the attendee WP_Post after updating attendee.
*
* @since TBD
*
* @param \WP_Post $attendee Post object for the attendee.
* @param \WP_Post $order Which order generated this attendee.
* @param Ticket_Object $ticket Which ticket generated this Attendee.
* @param array $args Set of extra arguments used to populate the data for the attendee.
*/
return apply_filters( 'tec_tickets_commerce_attendee_update', $attendee, $order, $ticket, $args );
}

/**
* If the post that was moved to the trash was a Tickets Commerce attendee post type, redirect to
* the Attendees Report rather than the Tickets Commerce attendees post list (because that's kind of
Expand Down
2 changes: 1 addition & 1 deletion src/Tickets/Commerce/Cart/Unmanaged_Cart.php
Original file line number Diff line number Diff line change
Expand Up @@ -124,7 +124,7 @@ public function clear() {

$this->set_hash( null );
delete_transient( Commerce\Cart::get_transient_name( $cart_hash ) );
tribe( Commerce\Cart::class )->set_cart_hash_cookie( $cart_hash );
tribe( Commerce\Cart::class )->set_cart_hash_cookie( null );

// clear cart items data.
$this->items = [];
Expand Down
19 changes: 18 additions & 1 deletion src/Tickets/Commerce/Flag_Actions/Generate_Attendees.php
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,23 @@ public function handle( Status_Interface $new_status, $old_status, \WP_Post $ord

$attendees = [];

$existing = [];
foreach (
tec_tc_attendees()->by_args(
[
'order_id' => $order->ID,
'ticket_id' => $ticket->ID,
'event_id' => $ticket->get_event_id(),
]
)->get_ids( true ) as $attendee_id
) {
if ( ! is_int( $attendee_id ) || 0 >= $attendee_id ) {
$existing[] = null;
continue;
}
$existing[] = $attendee_id;
Comment on lines +131 to +132
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
}
$existing[] = $attendee_id;
}
$existing[] = $attendee_id;

}

for ( $i = 0; $i < $quantity; $i ++ ) {
$args = [
'opt_out' => Arr::get( $extra, 'optout' ),
Expand All @@ -138,7 +155,7 @@ public function handle( Status_Interface $new_status, $old_status, \WP_Post $ord
*/
$args = apply_filters( 'tec_tickets_commerce_flag_action_generate_attendee_args', $args, $ticket, $order, $new_status, $old_status, $item, $i );

$attendee = tribe( Attendee::class )->create( $order, $ticket, $args );
$attendee = tribe( Attendee::class )->upsert( $order, $ticket, $args, $existing[ $i ] ?? null );

/**
* Fires after an attendee is generated for an order.
Expand Down
117 changes: 114 additions & 3 deletions src/Tickets/Commerce/Order.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
use TEC\Tickets\Commerce\Utils\Value;
use Tribe__Date_Utils as Dates;
use WP_Post;
use TEC\Tickets\Commerce\Status\Pending;

/**
* Class Order
Expand Down Expand Up @@ -387,6 +388,7 @@ static function ( $item ) {
* Creates a order from the items in the cart.
*
* @since 5.1.9
* @since TBD Now it will only create one order per cart hash. Every next time it will update the existing order.
*
* @throws \Tribe__Repository__Usage_Error
*
Expand Down Expand Up @@ -451,13 +453,16 @@ static function ( $item ) {

$total = $this->get_value_total( array_filter( $items ) );

$hash = $cart->get_cart_hash();
$existing_order_id = null;

$order_args = [
'title' => $this->generate_order_title( $original_cart_items, $cart->get_cart_hash() ),
'title' => $this->generate_order_title( $original_cart_items, $hash ),
'total_value' => $total->get_decimal(),
'subtotal' => $subtotal->get_decimal(),
'items' => $items,
'gateway' => $gateway::get_key(),
'hash' => $cart->get_cart_hash(),
'hash' => $hash,
'currency' => Utils\Currency::get_currency_code(),
'purchaser_user_id' => $purchaser['purchaser_user_id'],
'purchaser_full_name' => $purchaser['purchaser_full_name'],
Expand All @@ -466,7 +471,22 @@ static function ( $item ) {
'purchaser_email' => $purchaser['purchaser_email'],
];

$order = $this->create( $gateway, $order_args );
if ( $hash ) {
$existing_order_id = tec_tc_orders()->by_args(
[
'status' => tribe( Pending::class )->get_wp_slug(),
'hash' => $hash,
]
)->first_id();

if ( ! $existing_order_id || ! is_int( $existing_order_id ) ) {
$existing_order_id = null;
}
}

$order_args['id'] = $existing_order_id;

$order = $this->upsert( $gateway, $order_args );

// We were unable to create the order bail from here.
if ( ! $order ) {
Expand All @@ -481,6 +501,8 @@ static function ( $item ) {
*
* @since 5.2.0
*
* @internal Use `upsert` instead.
*
* @param Gateway_Interface $gateway
* @param array $args
*
Expand Down Expand Up @@ -514,6 +536,95 @@ public function create( Gateway_Interface $gateway, $args ) {
return tec_tc_orders()->set_args( $args )->create();
}

/**
* Filters the values and creates a new Order with Tickets Commerce or updates an existing one.
*
* @since TBD
*
* @param Gateway_Interface $gateway The gateway to use to create the order.
* @param array $args The arguments to create the order.
*
* @return false|WP_Post WP_Post instance on success or false on failure.
*/
public function upsert( Gateway_Interface $gateway, array $args ) {
$gateway_key = $gateway::get_key();

$existing_order_id = (int) $args['id'] ?? 0;
unset( $args['id'] );
Comment on lines +550 to +553
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
$gateway_key = $gateway::get_key();
$existing_order_id = (int) $args['id'] ?? 0;
unset( $args['id'] );
$gateway_key = $gateway::get_key();
$existing_order_id = (int) $args['id'] ?? 0;
unset( $args['id'] );


/**
* Allows filtering of the order upsert arguments for all orders created via Tickets Commerce.
*
* @since TBD
*
* @param array $args The arguments to create the order.
* @param Gateway_Interface $gateway The gateway to use to create the order.
*/
$args = apply_filters( "tec_tickets_commerce_order_{$gateway_key}_upsert_args", $args, $gateway );

/**
* Allows filtering of the order upsert arguments for all orders created via Tickets Commerce.
*
* @since TBD
*
* @param array $args The arguments to create the order.
* @param Gateway_Interface $gateway The gateway to use to create the order.
*/
$args = apply_filters( 'tec_tickets_commerce_order_upsert_args', $args, $gateway );

/**
* Allows filtering of the existing order ID before "upserting" an order.
*
* @since TDB
*
* @param int $existing_order_id The existing order ID.
*/
$existing_order_id = (int) apply_filters( 'tec_tickets_commerce_order_upsert_existing_order_id', $existing_order_id );

if ( ! $existing_order_id || 0 > $existing_order_id ) {
return $this->create( $gateway, $args );
}

/**
* Allows filtering of the order update arguments for all orders created via Tickets Commerce.
*
* @since TBD
*
* @param array $args
* @param Gateway_Interface $gateway
*/
$update_args = apply_filters( "tec_tickets_commerce_order_{$gateway_key}_update_args", $args, $gateway );

/**
* Allows filtering of the order update arguments for all orders created via Tickets Commerce.
*
* @since TBD
*
* @param array $args
* @param Gateway_Interface $gateway
*/
$update_args = apply_filters( 'tec_tickets_commerce_order_update_args', $update_args, $gateway );

$updated = tec_tc_orders()->where( 'id', $existing_order_id )->set_args( $update_args )->save();

if ( empty( $updated[ $existing_order_id ] ) ) {
/**
* It seems like the $existing_order_id no longer exists or failed to be updated. Let's create a new one instead.
*
* BE AWARE: THe variable $args here is not passed through the update filters since its going to pass through the create filters.
*/
return $this->create( $gateway, $args );
}

$order = tec_tc_get_order( $existing_order_id );

if ( ! $order instanceof WP_Post ) {
return false;
}

return $order;
}

/**
* Generates a title based on Cart Hash, items in the cart.
*
Expand Down
111 changes: 108 additions & 3 deletions tests/_data/ft_smoketest.sql

Large diffs are not rendered by default.

Loading
Loading