WC_Stripe_Order_Handler   C
last analyzed

Complexity

Total Complexity 54

Size/Duplication

Total Lines 371
Duplicated Lines 12.4 %

Coupling/Cohesion

Components 2
Dependencies 5

Importance

Changes 0
Metric Value
dl 46
loc 371
rs 6.4799
c 0
b 0
f 0
wmc 54
lcom 2
cbo 5

7 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 12 1
A get_instance() 0 3 1
F process_redirect_payment() 46 142 23
A maybe_process_redirect_order() 0 9 4
D capture_payment() 0 95 16
A cancel_payment() 0 13 3
B woocommerce_tracks_event_properties() 0 35 6

How to fix   Duplicated Code    Complexity   

Duplicated Code

Duplicate code is one of the most pungent code smells. A rule that is often used is to re-structure code once it is duplicated in three or more places.

Common duplication problems, and corresponding solutions are:

Complex Class

 Tip:   Before tackling complexity, make sure that you eliminate any duplication first. This often can reduce the size of classes significantly.

Complex classes like WC_Stripe_Order_Handler often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use WC_Stripe_Order_Handler, and based on these observations, apply Extract Interface, too.

1
<?php
2
if ( ! defined( 'ABSPATH' ) ) {
3
	exit;
4
}
5
6
/**
7
 * Handles and process orders from asyncronous flows.
8
 *
9
 * @since 4.0.0
10
 */
11
class WC_Stripe_Order_Handler extends WC_Stripe_Payment_Gateway {
12
	private static $_this;
13
	public $retry_interval;
14
15
	/**
16
	 * Constructor.
17
	 *
18
	 * @since 4.0.0
19
	 * @version 4.0.0
20
	 */
21
	public function __construct() {
22
		self::$_this = $this;
23
24
		$this->retry_interval = 1;
25
26
		add_action( 'wp', array( $this, 'maybe_process_redirect_order' ) );
27
		add_action( 'woocommerce_order_status_processing', array( $this, 'capture_payment' ) );
28
		add_action( 'woocommerce_order_status_completed', array( $this, 'capture_payment' ) );
29
		add_action( 'woocommerce_order_status_cancelled', array( $this, 'cancel_payment' ) );
30
		add_action( 'woocommerce_order_status_refunded', array( $this, 'cancel_payment' ) );
31
		add_filter( 'woocommerce_tracks_event_properties', array( $this, 'woocommerce_tracks_event_properties' ), 10, 2 );
32
	}
33
34
	/**
35
	 * Public access to instance object.
36
	 *
37
	 * @since 4.0.0
38
	 * @version 4.0.0
39
	 */
40
	public static function get_instance() {
41
		return self::$_this;
42
	}
43
44
	/**
45
	 * Processes payments.
46
	 * Note at this time the original source has already been
47
	 * saved to a customer card (if applicable) from process_payment.
48
	 *
49
	 * @since 4.0.0
50
	 * @since 4.1.8 Add $previous_error parameter.
51
	 * @param int $order_id
52
	 * @param bool $retry
53
	 * @param mix $previous_error Any error message from previous request.
54
	 */
55
	public function process_redirect_payment( $order_id, $retry = true, $previous_error = false ) {
56
		try {
57
			$source = wc_clean( $_GET['source'] );
58
59
			if ( empty( $source ) ) {
60
				return;
61
			}
62
63
			if ( empty( $order_id ) ) {
64
				return;
65
			}
66
67
			$order = wc_get_order( $order_id );
68
69
			if ( ! is_object( $order ) ) {
70
				return;
71
			}
72
73
			if ( $order->has_status( array( 'processing', 'completed', 'on-hold' ) ) ) {
74
				return;
75
			}
76
77
			// Result from Stripe API request.
78
			$response = null;
79
80
			// This will throw exception if not valid.
81
			$this->validate_minimum_order_amount( $order );
82
83
			WC_Stripe_Logger::log( "Info: (Redirect) Begin processing payment for order $order_id for the amount of {$order->get_total()}" );
84
85
			/**
86
			 * First check if the source is chargeable at this time. If not,
87
			 * webhook will take care of it later.
88
			 */
89
			$source_info = WC_Stripe_API::retrieve( 'sources/' . $source );
90
91
			if ( ! empty( $source_info->error ) ) {
92
				throw new WC_Stripe_Exception( print_r( $source_info, true ), $source_info->error->message );
93
			}
94
95
			if ( 'failed' === $source_info->status || 'canceled' === $source_info->status ) {
96
				throw new WC_Stripe_Exception( print_r( $source_info, true ), __( 'Unable to process this payment, please try again or use alternative method.', 'woocommerce-gateway-stripe' ) );
97
			}
98
99
			// If already consumed, then ignore request.
100
			if ( 'consumed' === $source_info->status ) {
101
				return;
102
			}
103
104
			// If not chargeable, then ignore request.
105
			if ( 'chargeable' !== $source_info->status ) {
106
				return;
107
			}
108
109
			// Prep source object.
110
			$source_object           = new stdClass();
111
			$source_object->token_id = '';
112
			$source_object->customer = $this->get_stripe_customer_id( $order );
113
			$source_object->source   = $source_info->id;
114
			$source_object->status   = 'chargeable';
115
116
			/* If we're doing a retry and source is chargeable, we need to pass
117
			 * a different idempotency key and retry for success.
118
			 */
119
			if ( $this->need_update_idempotency_key( $source_object, $previous_error ) ) {
120
				add_filter( 'wc_stripe_idempotency_key', array( $this, 'change_idempotency_key' ), 10, 2 );
121
			}
122
123
			// Make the request.
124
			$response = WC_Stripe_API::request( $this->generate_payment_request( $order, $source_object ), 'charges', 'POST', true );
125
			$headers  = $response['headers'];
126
			$response = $response['body'];
127
128 View Code Duplication
			if ( ! empty( $response->error ) ) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
129
				// Customer param wrong? The user may have been deleted on stripe's end. Remove customer_id. Can be retried without.
130
				if ( $this->is_no_such_customer_error( $response->error ) ) {
131
					delete_user_option( $order->get_customer_id(), '_stripe_customer_id' );
132
					$order->delete_meta_data( '_stripe_customer_id' );
133
					$order->save();
134
				}
135
136
				if ( $this->is_no_such_token_error( $response->error ) && $prepared_source->token_id ) {
0 ignored issues
show
Bug introduced by
The variable $prepared_source does not exist. Did you mean $source?

This check looks for variables that are accessed but have not been defined. It raises an issue if it finds another variable that has a similar name.

The variable may have been renamed without also renaming all references.

Loading history...
137
					// Source param wrong? The CARD may have been deleted on stripe's end. Remove token and show message.
138
					$wc_token = WC_Payment_Tokens::get( $prepared_source->token_id );
0 ignored issues
show
Bug introduced by
The variable $prepared_source does not exist. Did you mean $source?

This check looks for variables that are accessed but have not been defined. It raises an issue if it finds another variable that has a similar name.

The variable may have been renamed without also renaming all references.

Loading history...
139
					$wc_token->delete();
140
					$localized_message = __( 'This card is no longer available and has been removed.', 'woocommerce-gateway-stripe' );
141
					$order->add_order_note( $localized_message );
142
					throw new WC_Stripe_Exception( print_r( $response, true ), $localized_message );
143
				}
144
145
				// We want to retry.
146
				if ( $this->is_retryable_error( $response->error ) ) {
147
					if ( $retry ) {
148
						// Don't do anymore retries after this.
149
						if ( 5 <= $this->retry_interval ) {
150
							return $this->process_redirect_payment( $order_id, false, $response->error );
151
						}
152
153
						sleep( $this->retry_interval );
154
155
						$this->retry_interval++;
156
						return $this->process_redirect_payment( $order_id, true, $response->error );
157
					} else {
158
						$localized_message = __( 'Sorry, we are unable to process your payment at this time. Please retry later.', 'woocommerce-gateway-stripe' );
159
						$order->add_order_note( $localized_message );
160
						throw new WC_Stripe_Exception( print_r( $response, true ), $localized_message );
161
					}
162
				}
163
164
				$localized_messages = WC_Stripe_Helper::get_localized_messages();
165
166
				if ( 'card_error' === $response->error->type ) {
167
					$message = isset( $localized_messages[ $response->error->code ] ) ? $localized_messages[ $response->error->code ] : $response->error->message;
168
				} else {
169
					$message = isset( $localized_messages[ $response->error->type ] ) ? $localized_messages[ $response->error->type ] : $response->error->message;
170
				}
171
172
				throw new WC_Stripe_Exception( print_r( $response, true ), $message );
173
			}
174
175
			// To prevent double processing the order on WC side.
176
			if ( ! $this->is_original_request( $headers ) ) {
177
				return;
178
			}
179
180
			do_action( 'wc_gateway_stripe_process_redirect_payment', $response, $order );
181
182
			$this->process_response( $response, $order );
183
184
		} catch ( WC_Stripe_Exception $e ) {
185
			WC_Stripe_Logger::log( 'Error: ' . $e->getMessage() );
186
187
			do_action( 'wc_gateway_stripe_process_redirect_payment_error', $e, $order );
0 ignored issues
show
Bug introduced by
The variable $order does not seem to be defined for all execution paths leading up to this point.

If you define a variable conditionally, it can happen that it is not defined for all execution paths.

Let’s take a look at an example:

function myFunction($a) {
    switch ($a) {
        case 'foo':
            $x = 1;
            break;

        case 'bar':
            $x = 2;
            break;
    }

    // $x is potentially undefined here.
    echo $x;
}

In the above example, the variable $x is defined if you pass “foo” or “bar” as argument for $a. However, since the switch statement has no default case statement, if you pass any other value, the variable $x would be undefined.

Available Fixes

  1. Check for existence of the variable explicitly:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        if (isset($x)) { // Make sure it's always set.
            echo $x;
        }
    }
    
  2. Define a default value for the variable:

    function myFunction($a) {
        $x = ''; // Set a default which gets overridden for certain paths.
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        echo $x;
    }
    
  3. Add a value for the missing path:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
    
            // We add support for the missing case.
            default:
                $x = '';
                break;
        }
    
        echo $x;
    }
    
Loading history...
188
189
			/* translators: error message */
190
			$order->update_status( 'failed', sprintf( __( 'Stripe payment failed: %s', 'woocommerce-gateway-stripe' ), $e->getLocalizedMessage() ) );
191
192
			wc_add_notice( $e->getLocalizedMessage(), 'error' );
193
			wp_safe_redirect( wc_get_checkout_url() );
194
			exit;
195
		}
196
	}
197
198
	/**
199
	 * Processses the orders that are redirected.
200
	 *
201
	 * @since 4.0.0
202
	 * @version 4.0.0
203
	 */
204
	public function maybe_process_redirect_order() {
205
		if ( ! is_order_received_page() || empty( $_GET['client_secret'] ) || empty( $_GET['source'] ) ) {
206
			return;
207
		}
208
209
		$order_id = wc_clean( $_GET['order_id'] );
210
211
		$this->process_redirect_payment( $order_id );
212
	}
213
214
	/**
215
	 * Capture payment when the order is changed from on-hold to complete or processing.
216
	 *
217
	 * @since 3.1.0
218
	 * @version 4.0.0
219
	 * @param  int $order_id
220
	 */
221
	public function capture_payment( $order_id ) {
222
		$order = wc_get_order( $order_id );
223
224
		if ( 'stripe' === $order->get_payment_method() ) {
225
			$charge             = $order->get_transaction_id();
226
			$captured           = $order->get_meta( '_stripe_charge_captured', true );
227
			$is_stripe_captured = false;
228
229
			if ( $charge && 'no' === $captured ) {
230
				$order_total = $order->get_total();
231
232
				if ( 0 < $order->get_total_refunded() ) {
233
					$order_total = $order_total - $order->get_total_refunded();
234
				}
235
236
				$intent = $this->get_intent_from_order( $order );
237
				if ( $intent ) {
238
					// If the order has a Payment Intent, then the Intent itself must be captured, not the Charge
239
					if ( ! empty( $intent->error ) ) {
240
						/* translators: error message */
241
						$order->add_order_note( sprintf( __( 'Unable to capture charge! %s', 'woocommerce-gateway-stripe' ), $intent->error->message ) );
242
					} elseif ( 'requires_capture' === $intent->status ) {
243
						$level3_data = $this->get_level3_data_from_order( $order );
244
						$result = WC_Stripe_API::request_with_level3_data(
245
							array(
246
								'amount'   => WC_Stripe_Helper::get_stripe_amount( $order_total ),
247
								'expand[]' => 'charges.data.balance_transaction',
248
							),
249
							'payment_intents/' . $intent->id . '/capture',
250
							$level3_data,
251
							$order
252
						);
253
254
						if ( ! empty( $result->error ) ) {
255
							/* translators: error message */
256
							$order->update_status( 'failed', sprintf( __( 'Unable to capture charge! %s', 'woocommerce-gateway-stripe' ), $result->error->message ) );
257
						} else {
258
							$is_stripe_captured = true;
259
							$result = end( $result->charges->data );
260
						}
261
					} elseif ( 'succeeded' === $intent->status ) {
262
						$is_stripe_captured = true;
263
					}
264
				} else {
265
					// The order doesn't have a Payment Intent, fall back to capturing the Charge directly
266
267
					// First retrieve charge to see if it has been captured.
268
					$result = WC_Stripe_API::retrieve( 'charges/' . $charge );
269
270
					if ( ! empty( $result->error ) ) {
271
						/* translators: error message */
272
						$order->add_order_note( sprintf( __( 'Unable to capture charge! %s', 'woocommerce-gateway-stripe' ), $result->error->message ) );
273
					} elseif ( false === $result->captured ) {
274
						$level3_data = $this->get_level3_data_from_order( $order );
275
						$result = WC_Stripe_API::request_with_level3_data(
276
							array(
277
								'amount'   => WC_Stripe_Helper::get_stripe_amount( $order_total ),
278
								'expand[]' => 'balance_transaction',
279
							),
280
							'charges/' . $charge . '/capture',
281
							$level3_data,
282
							$order
283
						);
284
285
						if ( ! empty( $result->error ) ) {
286
							/* translators: error message */
287
							$order->update_status( 'failed', sprintf( __( 'Unable to capture charge! %s', 'woocommerce-gateway-stripe' ), $result->error->message ) );
288
						} else {
289
							$is_stripe_captured = true;
290
						}
291
					} elseif ( true === $result->captured ) {
292
						$is_stripe_captured = true;
293
					}
294
				}
295
296
				if ( $is_stripe_captured ) {
297
					/* translators: transaction id */
298
					$order->add_order_note( sprintf( __( 'Stripe charge complete (Charge ID: %s)', 'woocommerce-gateway-stripe' ), $result->id ) );
0 ignored issues
show
Bug introduced by
The variable $result does not seem to be defined for all execution paths leading up to this point.

If you define a variable conditionally, it can happen that it is not defined for all execution paths.

Let’s take a look at an example:

function myFunction($a) {
    switch ($a) {
        case 'foo':
            $x = 1;
            break;

        case 'bar':
            $x = 2;
            break;
    }

    // $x is potentially undefined here.
    echo $x;
}

In the above example, the variable $x is defined if you pass “foo” or “bar” as argument for $a. However, since the switch statement has no default case statement, if you pass any other value, the variable $x would be undefined.

Available Fixes

  1. Check for existence of the variable explicitly:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        if (isset($x)) { // Make sure it's always set.
            echo $x;
        }
    }
    
  2. Define a default value for the variable:

    function myFunction($a) {
        $x = ''; // Set a default which gets overridden for certain paths.
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        echo $x;
    }
    
  3. Add a value for the missing path:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
    
            // We add support for the missing case.
            default:
                $x = '';
                break;
        }
    
        echo $x;
    }
    
Loading history...
299
					$order->update_meta_data( '_stripe_charge_captured', 'yes' );
300
301
					// Store other data such as fees
302
					$order->set_transaction_id( $result->id );
303
304
					if ( is_callable( array( $order, 'save' ) ) ) {
305
						$order->save();
306
					}
307
308
					$this->update_fees( $order, $result->balance_transaction->id );
309
				}
310
311
				// This hook fires when admin manually changes order status to processing or completed.
312
				do_action( 'woocommerce_stripe_process_manual_capture', $order, $result );
313
			}
314
		}
315
	}
316
317
	/**
318
	 * Cancel pre-auth on refund/cancellation.
319
	 *
320
	 * @since 3.1.0
321
	 * @version 4.2.2
322
	 * @param  int $order_id
323
	 */
324
	public function cancel_payment( $order_id ) {
325
		$order = wc_get_order( $order_id );
326
327
		if ( 'stripe' === $order->get_payment_method() ) {
328
			$captured = $order->get_meta( '_stripe_charge_captured', true );
329
			if ( 'no' === $captured ) {
330
				$this->process_refund( $order_id );
331
			}
332
333
			// This hook fires when admin manually changes order status to cancel.
334
			do_action( 'woocommerce_stripe_process_manual_cancel', $order );
335
		}
336
	}
337
338
	/**
339
	 * Filter. Adds additional meta data to Tracks events.
340
	 * Note that this filter is only called if WC_Site_Tracking::is_tracking_enabled.
341
	 *
342
	 * @since 4.5.1
343
	 * @param array Properties to be appended to.
344
	 * @param string Event name, e.g. orders_edit_status_change.
345
	 */
346
	public function woocommerce_tracks_event_properties( $properties, $prefixed_event_name ) {
347
		// Not the desired event? Bail.
348
		if ( 'wcadmin_orders_edit_status_change' != $prefixed_event_name ) {
349
			return $properties;
350
		}
351
352
		// Properties not an array? Bail.
353
		if ( ! is_array( $properties ) ) {
354
			return $properties;
355
		}
356
357
		// No payment_method in properties? Bail.
358
		if ( ! array_key_exists( 'payment_method', $properties ) ) {
359
			return $properties;
360
		}
361
362
		// Not stripe? Bail.
363
		if ( 'stripe' != $properties[ 'payment_method' ] ) {
364
			return $properties;
365
		}
366
367
		// Due diligence done. Collect the metadata.
368
		$is_live         = true;
369
		$stripe_settings = get_option( 'woocommerce_stripe_settings', array() );
370
		if ( array_key_exists( 'testmode', $stripe_settings ) ) {
371
			$is_live = 'no' === $stripe_settings[ 'testmode' ];
372
		}
373
374
		$properties[ 'admin_email' ]                        = get_option( 'admin_email' );
375
		$properties[ 'is_live' ]                            = $is_live;
376
		$properties[ 'woocommerce_gateway_stripe_version' ] = WC_STRIPE_VERSION;
377
		$properties[ 'woocommerce_default_country' ]        = get_option( 'woocommerce_default_country' );
378
379
		return $properties;
380
	}
381
}
382
383
new WC_Stripe_Order_Handler();
384