Completed
Push — master ( 7e98cf...023f04 )
by Roy
02:08
created

WC_Stripe_Order_Handler::cancel_payment()   A

Complexity

Conditions 3
Paths 2

Size

Total Lines 10
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 3
eloc 5
nc 2
nop 1
dl 0
loc 10
rs 9.4285
c 0
b 0
f 0
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_on-hold_to_processing', array( $this, 'capture_payment' ) );
28
		add_action( 'woocommerce_order_status_on-hold_to_completed', array( $this, 'capture_payment' ) );
29
		add_action( 'woocommerce_order_status_on-hold_to_cancelled', array( $this, 'cancel_payment' ) );
30
		add_action( 'woocommerce_order_status_on-hold_to_refunded', array( $this, 'cancel_payment' ) );
31
		add_action( 'wc_ajax_wc_stripe_validate_checkout', array( $this, 'validate_checkout' ) );
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
	 * @version 4.0.0
51
	 */
52
	public function process_redirect_payment( $order_id, $retry = true ) {
53
		try {
54
			$source = wc_clean( $_GET['source'] );
55
56
			if ( empty( $source ) ) {
57
				return;
58
			}
59
60
			if ( empty( $order_id ) ) {
61
				return;
62
			}
63
64
			$order = wc_get_order( $order_id );
65
66
			if ( ! is_object( $order ) ) {
67
				return;
68
			}
69
70
			if ( 'processing' === $order->get_status() || 'completed' === $order->get_status() || 'on-hold' === $order->get_status() ) {
71
				return;
72
			}
73
74
			// Result from Stripe API request.
75
			$response = null;
76
77
			// This will throw exception if not valid.
78
			$this->validate_minimum_order_amount( $order );
79
80
			WC_Stripe_Logger::log( "Info: (Redirect) Begin processing payment for order $order_id for the amount of {$order->get_total()}" );
81
82
			/**
83
			 * First check if the source is chargeable at this time. If not,
84
			 * webhook will take care of it later.
85
			 */
86
			$source_info = WC_Stripe_API::retrieve( 'sources/' . $source );
87
88
			if ( ! empty( $source_info->error ) ) {
89
				throw new WC_Stripe_Exception( print_r( $source_info, true ), $source_info->error->message );
90
			}
91
92
			if ( 'failed' === $source_info->status || 'canceled' === $source_info->status ) {
93
				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' ) );
94
			}
95
96
			// If already consumed, then ignore request.
97
			if ( 'consumed' === $source_info->status ) {
98
				return;
99
			}
100
101
			// If not chargeable, then ignore request.
102
			if ( 'chargeable' !== $source_info->status ) {
103
				return;
104
			}
105
106
			// Prep source object.
107
			$source_object           = new stdClass();
108
			$source_object->token_id = '';
109
			$source_object->customer = $this->get_stripe_customer_id( $order );
110
			$source_object->source   = $source_info->id;
111
112
			/* If we're doing a retry and source is chargeable, we need to pass
113
			 * a different idempotency key and retry for success.
114
			 */
115 View Code Duplication
			if ( 1 < $this->retry_interval && 'chargeable' === $source_info->status ) {
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...
116
				add_filter( 'wc_stripe_idempotency_key', array( $this, 'change_idempotency_key' ), 10, 2 );
117
			}
118
119
			// Make the request.
120
			$response = WC_Stripe_API::request( $this->generate_payment_request( $order, $source_object ) );
121
122
			if ( ! empty( $response->error ) ) {
123
				// Customer param wrong? The user may have been deleted on stripe's end. Remove customer_id. Can be retried without.
124
				if ( preg_match( '/No such customer/i', $response->error->message ) && $retry ) {
125
					if ( WC_Stripe_Helper::is_pre_30() ) {
126
						delete_user_meta( $order->customer_user, '_stripe_customer_id' );
127
						delete_post_meta( $order_id, '_stripe_customer_id' );
128
					} else {
129
						delete_user_meta( $order->get_customer_id(), '_stripe_customer_id' );
130
						$order->delete_meta_data( '_stripe_customer_id' );
131
						$order->save();
132
					}
133
134
					return $this->process_redirect_payment( $order_id, false );
135
136
				} elseif ( preg_match( '/No such token/i', $response->error->message ) && $source_object->token_id ) {
137
					// Source param wrong? The CARD may have been deleted on stripe's end. Remove token and show message.
138
139
					$wc_token = WC_Payment_Tokens::get( $source_object->token_id );
140
					$wc_token->delete();
141
					$message = __( 'This card is no longer available and has been removed.', 'woocommerce-gateway-stripe' );
142
					$order->add_order_note( $message );
143
					throw new WC_Stripe_Exception( print_r( $response, true ), $message );
144
				}
145
146
				// We want to retry.
147
				if ( $this->is_retryable_error( $response->error ) ) {
148
					if ( $retry ) {
149
						// Don't do anymore retries after this.
150
						if ( 5 <= $this->retry_interval ) {
151
							return $this->process_redirect_payment( $order_id, false );
152
						}
153
154
						sleep( $this->retry_interval );
155
156
						$this->retry_interval++;
157
						return $this->process_redirect_payment( $order_id, true );
158
					} else {
159
						$localized_message = __( 'On going requests error and retries exhausted.', 'woocommerce-gateway-stripe' );
160
						$order->add_order_note( $localized_message );
161
						throw new WC_Stripe_Exception( print_r( $response, true ), $localized_message );
162
					}
163
				}
164
165
				$localized_messages = WC_Stripe_Helper::get_localized_messages();
166
167
				if ( 'card_error' === $response->error->type ) {
168
					$message = isset( $localized_messages[ $response->error->code ] ) ? $localized_messages[ $response->error->code ] : $response->error->message;
169
				} else {
170
					$message = isset( $localized_messages[ $response->error->type ] ) ? $localized_messages[ $response->error->type ] : $response->error->message;
171
				}
172
173
				throw new WC_Stripe_Exception( print_r( $response, true ), $message );
174
			}
175
176
			do_action( 'wc_gateway_stripe_process_redirect_payment', $response, $order );
177
178
			$this->process_response( $response, $order );
179
180
		} catch ( WC_Stripe_Exception $e ) {
181
			WC_Stripe_Logger::log( 'Error: ' . $e->getMessage() );
182
183
			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...
184
185
			/* translators: error message */
186
			$order->update_status( 'failed', sprintf( __( 'Stripe payment failed: %s', 'woocommerce-gateway-stripe' ), $e->getLocalizedMessage() ) );
187
188
			if ( $order->has_status( array( 'pending', 'failed' ) ) ) {
189
				$this->send_failed_order_email( $order_id );
190
			}
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' === ( WC_Stripe_Helper::is_pre_30() ? $order->payment_method : $order->get_payment_method() ) ) {
225
			$charge   = WC_Stripe_Helper::is_pre_30() ? get_post_meta( $order_id, '_transaction_id', true ) : $order->get_transaction_id();
226
			$captured = WC_Stripe_Helper::is_pre_30() ? get_post_meta( $order_id, '_stripe_charge_captured', true ) : $order->get_meta( '_stripe_charge_captured', true );
227
228
			if ( $charge && 'no' === $captured ) {
229
				$order_total = $order->get_total();
230
231
				if ( 0 < $order->get_total_refunded() ) {
232
					$order_total = $order_total - $order->get_total_refunded();
233
				}
234
235
				$result = WC_Stripe_API::request( array(
236
					'amount'   => WC_Stripe_Helper::get_stripe_amount( $order_total ),
237
					'expand[]' => 'balance_transaction',
238
				), 'charges/' . $charge . '/capture' );
239
240
				if ( ! empty( $result->error ) ) {
241
					/* translators: error message */
242
					$order->update_status( 'failed', sprintf( __( 'Unable to capture charge! %s', 'woocommerce-gateway-stripe' ), $result->error->message ) );
243
				} else {
244
					/* translators: transaction id */
245
					$order->add_order_note( sprintf( __( 'Stripe charge complete (Charge ID: %s)', 'woocommerce-gateway-stripe' ), $result->id ) );
246
					WC_Stripe_Helper::is_pre_30() ? update_post_meta( $order_id, '_stripe_charge_captured', 'yes' ) : $order->update_meta_data( '_stripe_charge_captured', 'yes' );
247
248
					// Store other data such as fees
249
					WC_Stripe_Helper::is_pre_30() ? update_post_meta( $order_id, '_transaction_id', $result->id ) : $order->set_transaction_id( $result->id );
250
251 View Code Duplication
					if ( isset( $result->balance_transaction ) && isset( $result->balance_transaction->fee ) ) {
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...
252
						// Fees and Net needs to both come from Stripe to be accurate as the returned
253
						// values are in the local currency of the Stripe account, not from WC.
254
						$fee = ! empty( $result->balance_transaction->fee ) ? WC_Stripe_Helper::format_balance_fee( $result->balance_transaction, 'fee' ) : 0;
255
						$net = ! empty( $result->balance_transaction->net ) ? WC_Stripe_Helper::format_balance_fee( $result->balance_transaction, 'net' ) : 0;
256
						WC_Stripe_Helper::is_pre_30() ? update_post_meta( $order_id, parent::META_NAME_FEE, $fee ) : $order->update_meta_data( parent::META_NAME_FEE, $fee );
257
						WC_Stripe_Helper::is_pre_30() ? update_post_meta( $order_id, parent::META_NAME_NET, $net ) : $order->update_meta_data( parent::META_NAME_NET, $net );
258
					}
259
260
					if ( is_callable( array( $order, 'save' ) ) ) {
261
						$order->save();
262
					}
263
				}
264
265
				// This hook fires when admin manually changes order status to processing or completed.
266
				do_action( 'woocommerce_stripe_process_manual_capture', $order, $result );
267
			}
268
		}
269
	}
270
271
	/**
272
	 * Cancel pre-auth on refund/cancellation.
273
	 *
274
	 * @since 3.1.0
275
	 * @version 4.0.0
276
	 * @param  int $order_id
277
	 */
278
	public function cancel_payment( $order_id ) {
279
		$order = wc_get_order( $order_id );
280
281
		if ( 'stripe' === ( WC_Stripe_Helper::is_pre_30() ? $order->payment_method : $order->get_payment_method() ) ) {
282
			$this->process_refund( $order_id );
283
284
			// This hook fires when admin manually changes order status to cancel.
285
			do_action( 'woocommerce_stripe_process_manual_cancel', $order );
286
		}
287
	}
288
289
	/**
290
	 * Validates the checkout before submitting checkout form.
291
	 *
292
	 * @since 4.0.0
293
	 * @version 4.0.0
294
	 */
295
	public function validate_checkout() {
296
		if ( ! wp_verify_nonce( $_POST['nonce'], '_wc_stripe_nonce' ) ) {
297
			wp_die( __( 'Cheatin&#8217; huh?', 'woocommerce-gateway-stripe' ) );
298
		}
299
300
		/*
301
		 * Client expects json encoded results to be "success" or message of HTML errors.
302
		 * i.e. wp_send_json( 'success' ); // On successful validation.
303
		 * i.e. For errors follow WC https://github.com/woocommerce/woocommerce/blob/master/includes/class-wc-checkout.php#L918-L938
304
		 */
305
		do_action( 'wc_stripe_validate_modal_checkout_action', $_POST['required_fields'], $_POST['all_fields'] );
306
	}
307
}
308
309
new WC_Stripe_Order_Handler();
310