Skip to content

Commit

Permalink
Merge pull request #3477 from the-events-calendar/feat/introduce-lock…
Browse files Browse the repository at this point in the history
…-order-mechanism

Introduce Lock Order Mechanism
  • Loading branch information
dpanta94 authored Jan 10, 2025
2 parents cb836e6 + 8a98c91 commit 5caf507
Show file tree
Hide file tree
Showing 12 changed files with 631 additions and 20 deletions.
4 changes: 4 additions & 0 deletions changelog/feat-introduce-lock-order-mechanism
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
Significance: minor
Type: fix

Introduce Order lock mechanism to ensure 2 or more action that could update the order, they dont so at the same time. [ET-2279]
67 changes: 67 additions & 0 deletions src/Tickets/Commerce/Gateways/Stripe/Hooks.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,11 @@
use Tribe\Tickets\Admin\Settings as Admin_Settings;
use Tribe\Admin\Pages;
use Tribe__Tickets__Main as Tickets_Plugin;
use WP_Post;
use Exception;
use TEC\Tickets\Commerce\Order;
use TEC\Tickets\Commerce\Status\Status_Handler;
use TEC\Tickets\Commerce\Gateways\Stripe\Webhooks;

/**
* Class Hooks
Expand Down Expand Up @@ -44,6 +49,8 @@ protected function add_actions() {
add_action( 'wp_ajax_tec_tickets_commerce_gateway_stripe_verify_webhooks', [ $this, 'action_handle_verify_webhooks' ] );

add_action( 'wp_ajax_' . Webhooks::NONCE_KEY_SETUP, [ $this, 'action_handle_set_up_webhook' ] );

add_action( 'tec_tickets_commerce_async_webhook_process', [ $this, 'process_async_stripe_webhook' ], 10 );
}

/**
Expand All @@ -61,6 +68,66 @@ protected function add_filters() {
add_filter( 'tec_tickets_commerce_admin_notices', [ $this, 'filter_admin_notices' ] );
}

/**
* Process the async stripe webhook.
*
* @since TBD
*
* @param int $order_id The order ID.
*
* @throws Exception If the action fails after too many retries.
*/
public function process_async_stripe_webhook( int $order_id ): void {
$order = tec_tc_get_order( $order_id );

if ( ! $order ) {
return;
}

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

if ( ! $order->ID ) {
return;
}

$webhooks = tribe( Webhooks::class );

$pending_webhooks = $webhooks->get_pending_webhooks( $order->ID );

// On multiple checkout completes, make sure we dont process the same webhook twice.
$webhooks->delete_pending_webhooks( $order->ID );

foreach ( $pending_webhooks as $pending_webhook ) {
if ( ! ( is_array( $pending_webhook ) ) ) {
continue;
}

if ( ! isset( $pending_webhook['new_status'], $pending_webhook['metadata'], $pending_webhook['old_status'] ) ) {
continue;
}

$new_status_wp_slug = $pending_webhook['new_status'];

// The order is already there!
if ( $order->post_status === $new_status_wp_slug ) {
continue;
}

// The order is no longer where it was... that could be dangerous, lets bail?
if ( $order->post_status !== $pending_webhook['old_status'] ) {
continue;
}

tribe( Order::class )->modify_status(
$order->ID,
tribe( Status_Handler::class )->get_by_wp_slug( $new_status_wp_slug )->get_slug(),
$pending_webhook['metadata']
);
}
}

/**
* Set up Stripe Webhook based on transient value.
*
Expand Down
37 changes: 27 additions & 10 deletions src/Tickets/Commerce/Gateways/Stripe/REST/Order_Endpoint.php
Original file line number Diff line number Diff line change
Expand Up @@ -108,15 +108,16 @@ public function handle_create_order( WP_REST_Request $request ) {
'success' => false,
];

$orders = tribe( Order::class );
$messages = $this->get_error_messages();
$data = $request->get_json_params();
$purchaser = tribe( Order::class )->get_purchaser_data( $data );
$purchaser = $orders->get_purchaser_data( $data );

if ( is_wp_error( $purchaser ) ) {
return $purchaser;
}

$order = tribe( Order::class )->create_from_cart( tribe( Gateway::class ), $purchaser );
$order = $orders->create_from_cart( tribe( Gateway::class ), $purchaser );

$payment_intent = tribe( Payment_Intent_Handler::class )->update_payment_intent( $data, $order );

Expand All @@ -129,12 +130,17 @@ public function handle_create_order( WP_REST_Request $request ) {
}

// Orders need to pass the Pending status always.
$updated = tribe( Order::class )->modify_status( $order->ID, Pending::SLUG, [
'gateway_payload' => $payment_intent,
'gateway_order_id' => $payment_intent['id'],
] );
$updated = $orders->modify_status(
$order->ID,
Pending::SLUG,
[
'gateway_payload' => $payment_intent,
'gateway_order_id' => $payment_intent['id'],
]
);

if ( is_wp_error( $updated ) ) {
$orders->checkout_completed( $order->ID );
return $updated;
}

Expand All @@ -144,6 +150,8 @@ public function handle_create_order( WP_REST_Request $request ) {
$response['client_secret'] = $payment_intent['client_secret'];
$response['redirect_url'] = add_query_arg( [ 'tc-order-id' => $payment_intent['id'] ], tribe( Success::class )->get_url() );

$orders->checkout_completed( $order->ID );

return new WP_REST_Response( $response );
}

Expand Down Expand Up @@ -233,12 +241,19 @@ public function handle_update_order( WP_REST_Request $request ) {
return new WP_Error( 'tec-tc-gateway-stripe-invalid-payment-intent-status', $messages['invalid-payment-intent-status'], $payment_intent_status );
}

$updated = tribe( Order::class )->modify_status( $order->ID, $status->get_slug(), [
'gateway_payload' => $payment_intent,
'gateway_order_id' => $payment_intent['id'],
] );
$orders = tribe( Order::class );

$updated = $orders->modify_status(
$order->ID,
$status->get_slug(),
[
'gateway_payload' => $payment_intent,
'gateway_order_id' => $payment_intent['id'],
]
);

if ( is_wp_error( $updated ) ) {
$orders->checkout_completed( $order->ID );
return $updated;
}

Expand All @@ -253,6 +268,8 @@ public function handle_update_order( WP_REST_Request $request ) {

$response['redirect_url'] = add_query_arg( [ 'tc-order-id' => $gateway_order_id ], tribe( Success::class )->get_url() );

$orders->checkout_completed( $order->ID );

return new WP_REST_Response( $response );
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -89,13 +89,20 @@ public function handle_incoming_request( WP_REST_Request $request ) {
* Given a WP Rest request we determine if it has the correct Stripe signature.
*
* @since 5.3.0
* @since TBD - Protect from fatal when header is missing.
*
* @param WP_REST_Request $request Which request we are validating.
*
* @return bool
*/
public function verify_incoming_request_permission( WP_REST_Request $request ): bool {
return $this->signature_is_valid( $request->get_header( 'Stripe-Signature' ), $request->get_body() );
$header = $request->get_header( 'Stripe-Signature' );

if ( ! $header || ! is_string( $header ) ) {
return false;
}

return $this->signature_is_valid( $header, $request->get_body() );
}

/**
Expand Down
59 changes: 59 additions & 0 deletions src/Tickets/Commerce/Gateways/Stripe/Webhooks.php
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,15 @@ class Webhooks extends Abstract_Webhooks {
*/
public const OPTION_KNOWN_WEBHOOKS = 'tickets-commerce-stripe-known-webhooks';

/**
* Option name for the option to store pending webhooks.
*
* @since TBD
*
* @var string
*/
public const PENDING_WEBHOOKS_KEY = '_tec_tickets_commerce_stripe_webhook_pending';

/**
* Nonce key for webhook on-demand set up.
*
Expand All @@ -89,6 +98,56 @@ public function get_merchant(): Abstract_Merchant {
return tribe( Merchant::class );
}

/**
* Add a pending webhook to the order.
*
* @since TBD
*
* @param int $order_id Order ID.
* @param string $new_status New status.
* @param string $old_status Old status.
* @param array $metadata Metadata.
*
* @return void
*/
public function add_pending_webhook( int $order_id, string $new_status, string $old_status, array $metadata = [] ): void {
add_post_meta(
$order_id,
self::PENDING_WEBHOOKS_KEY,
[
'new_status' => $new_status,
'metadata' => $metadata,
'old_status' => $old_status,
]
);
}

/**
* Get the pending webhooks for an order.
*
* @since TBD
*
* @param int $order_id Order ID.
*
* @return array
*/
public function get_pending_webhooks( int $order_id ): array {
return (array) get_post_meta( $order_id, self::PENDING_WEBHOOKS_KEY );
}

/**
* Delete the pending webhooks for an order.
*
* @since TBD
*
* @param int $order_id Order ID.
*
* @return void
*/
public function delete_pending_webhooks( int $order_id ): void {
delete_post_meta( $order_id, self::PENDING_WEBHOOKS_KEY );
}

/**
* Attempts to get the database option for the valid key from Stripe
* This function was introduced to enable a cache-free polling of the database for the Valid Key, it will include a
Expand Down
16 changes: 15 additions & 1 deletion src/Tickets/Commerce/Gateways/Stripe/Webhooks/Handler.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
use WP_Error;
use WP_REST_Request;
use WP_REST_Response;
use TEC\Tickets\Commerce\Gateways\Stripe\Webhooks;

/**
* Class Handler
Expand Down Expand Up @@ -129,6 +130,19 @@ public static function get_handler_method_for_event( $type ) {
* @return bool|WP_Error|null
*/
public static function update_order_status( \WP_Post $order, Commerce_Status\Status_Interface $status, array $metadata = [] ) {
if ( ! tribe( Order::class )->is_checkout_completed( $order->ID ) ) {

tribe( Webhooks::class )->add_pending_webhook( $order->ID, $status->get_wp_slug(), $order->post_status, $metadata );

/**
* We can't return WP_Error because that will make Stripe think that
* we failed to process the Webhook and as a result will resend it.
*
* Returning bool is the best option here. False since we didn't update, but we will!
*/
return false;
}

return tribe( Order::class )->modify_status( $order->ID, $status->get_slug(), $metadata );
}
}
}
Loading

0 comments on commit 5caf507

Please sign in to comment.