Completed
Push — master ( e3a5fe...3adadd )
by Roy
05:10
created

WC_Stripe_Order_Handler::get_instance()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 3
rs 10
c 0
b 0
f 0
cc 1
eloc 2
nc 1
nop 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
	}
32
33
	/**
34
	 * Public access to instance object.
35
	 *
36
	 * @since 4.0.0
37
	 * @version 4.0.0
38
	 */
39
	public static function get_instance() {
40
		return self::$_this;
41
	}
42
43
	/**
44
	 * Processes payments.
45
	 * Note at this time the original source has already been
46
	 * saved to a customer card (if applicable) from process_payment.
47
	 *
48
	 * @since 4.0.0
49
	 * @version 4.0.0
50
	 */
51
	public function process_redirect_payment( $order_id, $retry = true ) {
52
		try {
53
			$source = wc_clean( $_GET['source'] );
54
55
			if ( empty( $source ) ) {
56
				return;
57
			}
58
59
			if ( empty( $order_id ) ) {
60
				return;
61
			}
62
63
			$order = wc_get_order( $order_id );
64
65
			if ( ! is_object( $order ) ) {
66
				return;
67
			}
68
69
			if ( 'processing' === $order->get_status() || 'completed' === $order->get_status() || 'on-hold' === $order->get_status() ) {
70
				return;
71
			}
72
73
			// Result from Stripe API request.
74
			$response = null;
75
76
			// This will throw exception if not valid.
77
			$this->validate_minimum_order_amount( $order );
78
79
			WC_Stripe_Logger::log( "Info: (Redirect) Begin processing payment for order $order_id for the amount of {$order->get_total()}" );
80
81
			/**
82
			 * First check if the source is chargeable at this time. If not,
83
			 * webhook will take care of it later.
84
			 */
85
			$source_info = WC_Stripe_API::retrieve( 'sources/' . $source );
86
87
			if ( ! empty( $source_info->error ) ) {
88
				throw new WC_Stripe_Exception( print_r( $source_info, true ), $source_info->error->message );
89
			}
90
91
			if ( 'failed' === $source_info->status || 'canceled' === $source_info->status ) {
92
				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' ) );
93
			}
94
95
			// If already consumed, then ignore request.
96
			if ( 'consumed' === $source_info->status ) {
97
				return;
98
			}
99
100
			// If not chargeable, then ignore request.
101
			if ( 'chargeable' !== $source_info->status ) {
102
				return;
103
			}
104
105
			// Prep source object.
106
			$source_object           = new stdClass();
107
			$source_object->token_id = '';
108
			$source_object->customer = $this->get_stripe_customer_id( $order );
109
			$source_object->source   = $source_info->id;
110
111
			/* If we're doing a retry and source is chargeable, we need to pass
112
			 * a different idempotency key and retry for success.
113
			 */
114
			if ( 1 < $this->retry_interval && 'chargeable' === $source_info->status ) {
115
				add_filter( 'wc_stripe_idempotency_key', array( $this, 'change_idempotency_key' ), 10, 2 );
116
			}
117
118
			// Make the request.
119
			$response = WC_Stripe_API::request( $this->generate_payment_request( $order, $source_object ), 'charges', 'POST', true );
120
			$headers  = $response['headers'];
121
			$response = $response['body'];
122
123 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...
124
				// Customer param wrong? The user may have been deleted on stripe's end. Remove customer_id. Can be retried without.
125
				if ( $this->is_no_such_customer_error( $response->error ) ) {
126
					if ( WC_Stripe_Helper::is_pre_30() ) {
127
						delete_user_meta( $order->customer_user, '_stripe_customer_id' );
128
						delete_post_meta( $order_id, '_stripe_customer_id' );
129
					} else {
130
						delete_user_meta( $order->get_customer_id(), '_stripe_customer_id' );
131
						$order->delete_meta_data( '_stripe_customer_id' );
132
						$order->save();
133
					}
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 );
151
						}
152
153
						sleep( $this->retry_interval );
154
155
						$this->retry_interval++;
156
						return $this->process_redirect_payment( $order_id, true );
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
			if ( $order->has_status( array( 'pending', 'failed' ) ) ) {
193
				$this->send_failed_order_email( $order_id );
194
			}
195
196
			wc_add_notice( $e->getLocalizedMessage(), 'error' );
197
			wp_safe_redirect( wc_get_checkout_url() );
198
			exit;
199
		}
200
	}
201
202
	/**
203
	 * Processses the orders that are redirected.
204
	 *
205
	 * @since 4.0.0
206
	 * @version 4.0.0
207
	 */
208
	public function maybe_process_redirect_order() {
209
		if ( ! is_order_received_page() || empty( $_GET['client_secret'] ) || empty( $_GET['source'] ) ) {
210
			return;
211
		}
212
213
		$order_id = wc_clean( $_GET['order_id'] );
214
215
		$this->process_redirect_payment( $order_id );
216
	}
217
218
	/**
219
	 * Capture payment when the order is changed from on-hold to complete or processing.
220
	 *
221
	 * @since 3.1.0
222
	 * @version 4.0.0
223
	 * @param  int $order_id
224
	 */
225
	public function capture_payment( $order_id ) {
226
		$order = wc_get_order( $order_id );
227
228
		if ( 'stripe' === ( WC_Stripe_Helper::is_pre_30() ? $order->payment_method : $order->get_payment_method() ) ) {
229
			$charge   = WC_Stripe_Helper::is_pre_30() ? get_post_meta( $order_id, '_transaction_id', true ) : $order->get_transaction_id();
230
			$captured = WC_Stripe_Helper::is_pre_30() ? get_post_meta( $order_id, '_stripe_charge_captured', true ) : $order->get_meta( '_stripe_charge_captured', true );
231
232
			if ( $charge && 'no' === $captured ) {
233
				$order_total = $order->get_total();
234
235
				if ( 0 < $order->get_total_refunded() ) {
236
					$order_total = $order_total - $order->get_total_refunded();
237
				}
238
239
				$result = WC_Stripe_API::request( array(
240
					'amount'   => WC_Stripe_Helper::get_stripe_amount( $order_total ),
241
					'expand[]' => 'balance_transaction',
242
				), 'charges/' . $charge . '/capture' );
243
244
				if ( ! empty( $result->error ) ) {
245
					/* translators: error message */
246
					$order->update_status( 'failed', sprintf( __( 'Unable to capture charge! %s', 'woocommerce-gateway-stripe' ), $result->error->message ) );
247
				} else {
248
					/* translators: transaction id */
249
					$order->add_order_note( sprintf( __( 'Stripe charge complete (Charge ID: %s)', 'woocommerce-gateway-stripe' ), $result->id ) );
250
					WC_Stripe_Helper::is_pre_30() ? update_post_meta( $order_id, '_stripe_charge_captured', 'yes' ) : $order->update_meta_data( '_stripe_charge_captured', 'yes' );
251
252
					// Store other data such as fees
253
					WC_Stripe_Helper::is_pre_30() ? update_post_meta( $order_id, '_transaction_id', $result->id ) : $order->set_transaction_id( $result->id );
254
255
					if ( isset( $result->balance_transaction ) && isset( $result->balance_transaction->fee ) ) {
256
						// Fees and Net needs to both come from Stripe to be accurate as the returned
257
						// values are in the local currency of the Stripe account, not from WC.
258
						$fee = ! empty( $result->balance_transaction->fee ) ? WC_Stripe_Helper::format_balance_fee( $result->balance_transaction, 'fee' ) : 0;
259
						$net = ! empty( $result->balance_transaction->net ) ? WC_Stripe_Helper::format_balance_fee( $result->balance_transaction, 'net' ) : 0;
260
						WC_Stripe_Helper::update_stripe_fee( $order, $fee );
261
						WC_Stripe_Helper::update_stripe_net( $order, $net );
262
					}
263
264
					if ( is_callable( array( $order, 'save' ) ) ) {
265
						$order->save();
266
					}
267
				}
268
269
				// This hook fires when admin manually changes order status to processing or completed.
270
				do_action( 'woocommerce_stripe_process_manual_capture', $order, $result );
271
			}
272
		}
273
	}
274
275
	/**
276
	 * Cancel pre-auth on refund/cancellation.
277
	 *
278
	 * @since 3.1.0
279
	 * @version 4.0.0
280
	 * @param  int $order_id
281
	 */
282
	public function cancel_payment( $order_id ) {
283
		$order = wc_get_order( $order_id );
284
285
		if ( 'stripe' === ( WC_Stripe_Helper::is_pre_30() ? $order->payment_method : $order->get_payment_method() ) ) {
286
			$this->process_refund( $order_id );
287
288
			// This hook fires when admin manually changes order status to cancel.
289
			do_action( 'woocommerce_stripe_process_manual_cancel', $order );
290
		}
291
	}
292
}
293
294
new WC_Stripe_Order_Handler();
295