Completed
Push — master ( 023f04...fecf0e )
by Roy
12:15
created

WC_Stripe_Payment_Gateway::is_original_request()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 7
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 2
eloc 4
nc 2
nop 1
dl 0
loc 7
rs 9.4285
c 0
b 0
f 0
1
<?php
2
if ( ! defined( 'ABSPATH' ) ) {
3
	exit;
4
}
5
6
/**
7
 * Abstract class that will be inherited by all payment methods.
8
 *
9
 * @extends WC_Payment_Gateway_CC
10
 *
11
 * @since 4.0.0
12
 */
13
abstract class WC_Stripe_Payment_Gateway extends WC_Payment_Gateway_CC {
14
	const META_NAME_FEE = 'Stripe Fee';
15
	const META_NAME_NET = 'Net Revenue From Stripe';
16
17
	/**
18
	 * Checks to see if request is invalid and that
19
	 * they are worth retrying.
20
	 *
21
	 * @since 4.0.5
22
	 * @param array $error
23
	 */
24
	public function is_retryable_error( $error ) {
25
		return (
26
			'invalid_request_error' === $error->type ||
27
			'idempotency_error' === $error->type ||
28
			'rate_limit_error' === $error->type ||
29
			'api_connection_error' === $error->type ||
30
			'api_error' === $error->type
31
		);
32
	}
33
34
	/**
35
	 * Check if this gateway is enabled
36
	 */
37
	public function is_available() {
38
		if ( 'yes' === $this->enabled ) {
39
			if ( ! $this->testmode && is_checkout() && ! is_ssl() ) {
40
				return false;
41
			}
42
			if ( ! $this->secret_key || ! $this->publishable_key ) {
43
				return false;
44
			}
45
			return true;
46
		}
47
48
		return parent::is_available();
49
	}
50
51
	/**
52
	 * Allow this class and other classes to add slug keyed notices (to avoid duplication).
53
	 *
54
	 * @since 4.0.0
55
	 * @version 4.0.0
56
	 */
57
	public function add_admin_notice( $slug, $class, $message ) {
58
		$this->notices[ $slug ] = array(
59
			'class'   => $class,
60
			'message' => $message,
61
		);
62
	}
63
64
	/**
65
	 * Remove admin notice.
66
	 *
67
	 * @since 4.0.0
68
	 * @version 4.0.0
69
	 */
70
	public function remove_admin_notice() {
71
		if ( did_action( 'woocommerce_update_options' ) ) {
72
			remove_action( 'admin_notices', array( $this, 'check_environment' ) );
73
		}
74
	}
75
76
	/**
77
	 * All payment icons that work with Stripe.
78
	 *
79
	 * @since 4.0.0
80
	 * @version 4.0.0
81
	 * @return array
82
	 */
83
	public function payment_icons() {
84
		return apply_filters( 'wc_stripe_payment_icons', array(
85
			'visa'       => '<i class="stripe-pf stripe-pf-visa stripe-pf-right" alt="Visa" aria-hidden="true"></i>',
86
			'amex'       => '<i class="stripe-pf stripe-pf-american-express stripe-pf-right" alt="Amex" aria-hidden="true"></i>',
87
			'mastercard' => '<i class="stripe-pf stripe-pf-mastercard stripe-pf-right" alt="Mastercard" aria-hidden="true"></i>',
88
			'discover'   => '<i class="stripe-pf stripe-pf-discover stripe-pf-right" alt="Discover" aria-hidden="true"></i>',
89
			'diners'     => '<i class="stripe-pf stripe-pf-diners stripe-pf-right" alt="Diners" aria-hidden="true"></i>',
90
			'jcb'        => '<i class="stripe-pf stripe-pf-jcb stripe-pf-right" alt="JCB" aria-hidden="true"></i>',
91
			'alipay'     => '<i class="stripe-pf stripe-pf-alipay stripe-pf-right" alt="Alipay" aria-hidden="true"></i>',
92
			'wechat'     => '<i class="stripe-pf stripe-pf-wechat-pay stripe-pf-right" alt="Wechat Pay" aria-hidden="true"></i>',
93
			'bitcoin'    => '<i class="stripe-pf stripe-pf-bitcoin stripe-pf-right" alt="Bitcoin" aria-hidden="true"></i>',
94
			'bancontact' => '<i class="stripe-pf stripe-pf-bancontact-mister-cash stripe-pf-right" alt="Bancontact" aria-hidden="true"></i>',
95
			'ideal'      => '<i class="stripe-pf stripe-pf-ideal stripe-pf-right" alt="iDeal" aria-hidden="true"></i>',
96
			'p24'        => '<i class="stripe-pf stripe-pf-p24 stripe-pf-right" alt="P24" aria-hidden="true"></i>',
97
			'giropay'    => '<i class="stripe-pf stripe-pf-giropay stripe-pf-right" alt="Giropay" aria-hidden="true"></i>',
98
			'eps'        => '<i class="stripe-pf stripe-pf-eps stripe-pf-right" alt="EPS" aria-hidden="true"></i>',
99
			'sofort'     => '<i class="stripe-pf stripe-pf-sofort stripe-pf-right" alt="SOFORT" aria-hidden="true"></i>',
100
			'sepa'       => '<i class="stripe-pf stripe-pf-sepa stripe-pf-right" alt="SEPA" aria-hidden="true"></i>',
101
		) );
102
	}
103
104
	/**
105
	 * Validates that the order meets the minimum order amount
106
	 * set by Stripe.
107
	 *
108
	 * @since 4.0.0
109
	 * @version 4.0.0
110
	 * @param object $order
111
	 */
112
	public function validate_minimum_order_amount( $order ) {
113
		if ( $order->get_total() * 100 < WC_Stripe_Helper::get_minimum_amount() ) {
114
			/* translators: 1) dollar amount */
115
			throw new WC_Stripe_Exception( 'Did not meet minimum amount', sprintf( __( 'Sorry, the minimum allowed order total is %1$s to use this payment method.', 'woocommerce-gateway-stripe' ), wc_price( WC_Stripe_Helper::get_minimum_amount() / 100 ) ) );
116
		}
117
	}
118
119
	/**
120
	 * Gets the transaction URL linked to Stripe dashboard.
121
	 *
122
	 * @since 4.0.0
123
	 * @version 4.0.0
124
	 */
125
	public function get_transaction_url( $order ) {
126
		if ( $this->testmode ) {
127
			$this->view_transaction_url = 'https://dashboard.stripe.com/test/payments/%s';
128
		} else {
129
			$this->view_transaction_url = 'https://dashboard.stripe.com/payments/%s';
130
		}
131
132
		return parent::get_transaction_url( $order );
133
	}
134
135
	/**
136
	 * Gets the saved customer id if exists.
137
	 *
138
	 * @since 4.0.0
139
	 * @version 4.0.0
140
	 */
141
	public function get_stripe_customer_id( $order ) {
142
		$customer = get_user_meta( WC_Stripe_Helper::is_pre_30() ? $order->customer_user : $order->get_customer_id(), '_stripe_customer_id', true );
143
144
		if ( empty( $customer ) ) {
145
			// Try to get it via the order.
146
			if ( WC_Stripe_Helper::is_pre_30() ) {
147
				return get_post_meta( $order->id, '_stripe_customer_id', true );
148
			} else {
149
				return $order->get_meta( '_stripe_customer_id', true );
150
			}
151
		} else {
152
			return $customer;
153
		}
154
155
		return false;
0 ignored issues
show
Unused Code introduced by
return false; does not seem to be reachable.

This check looks for unreachable code. It uses sophisticated control flow analysis techniques to find statements which will never be executed.

Unreachable code is most often the result of return, die or exit statements that have been added for debug purposes.

function fx() {
    try {
        doSomething();
        return true;
    }
    catch (\Exception $e) {
        return false;
    }

    return false;
}

In the above example, the last return false will never be executed, because a return statement has already been met in every possible execution path.

Loading history...
156
	}
157
158
	/**
159
	 * Builds the return URL from redirects.
160
	 *
161
	 * @since 4.0.0
162
	 * @version 4.0.0
163
	 * @param object $order
164
	 * @param int $id Stripe session id.
165
	 */
166
	public function get_stripe_return_url( $order = null, $id = null ) {
167
		if ( is_object( $order ) ) {
168
			if ( empty( $id ) ) {
169
				$id = uniqid();
0 ignored issues
show
Unused Code introduced by
$id is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
170
			}
171
172
			$order_id = WC_Stripe_Helper::is_pre_30() ? $order->id : $order->get_id();
173
174
			$args = array(
175
				'utm_nooverride' => '1',
176
				'order_id'       => $order_id,
177
			);
178
179
			return esc_url_raw( add_query_arg( $args, $this->get_return_url( $order ) ) );
180
		}
181
182
		return esc_url_raw( add_query_arg( array( 'utm_nooverride' => '1' ), $this->get_return_url() ) );
183
	}
184
185
	/**
186
	 * Is $order_id a subscription?
187
	 * @param  int  $order_id
188
	 * @return boolean
189
	 */
190
	public function has_subscription( $order_id ) {
191
		return ( function_exists( 'wcs_order_contains_subscription' ) && ( wcs_order_contains_subscription( $order_id ) || wcs_is_subscription( $order_id ) || wcs_order_contains_renewal( $order_id ) ) );
192
	}
193
194
	/**
195
	 * Generate the request for the payment.
196
	 *
197
	 * @since 3.1.0
198
	 * @version 4.0.0
199
	 * @param  WC_Order $order
200
	 * @param  object $prepared_source
201
	 * @return array()
0 ignored issues
show
Documentation introduced by
The doc-type array() could not be parsed: Expected "|" or "end of type", but got "(" at position 5. (view supported doc-types)

This check marks PHPDoc comments that could not be parsed by our parser. To see which comment annotations we can parse, please refer to our documentation on supported doc-types.

Loading history...
202
	 */
203
	public function generate_payment_request( $order, $prepared_source ) {
204
		$settings                          = get_option( 'woocommerce_stripe_settings', array() );
205
		$statement_descriptor              = ! empty( $settings['statement_descriptor'] ) ? str_replace( "'", '', $settings['statement_descriptor'] ) : '';
206
		$capture                           = ! empty( $settings['capture'] ) && 'yes' === $settings['capture'] ? true : false;
207
		$post_data                         = array();
208
		$post_data['currency']             = strtolower( WC_Stripe_Helper::is_pre_30() ? $order->get_order_currency() : $order->get_currency() );
209
		$post_data['amount']               = WC_Stripe_Helper::get_stripe_amount( $order->get_total(), $post_data['currency'] );
210
		/* translators: 1) blog name 2) order number */
211
		$post_data['description']          = sprintf( __( '%1$s - Order %2$s', 'woocommerce-gateway-stripe' ), wp_specialchars_decode( get_bloginfo( 'name' ), ENT_QUOTES ), $order->get_order_number() );
212
		$billing_email      = WC_Stripe_Helper::is_pre_30() ? $order->billing_email : $order->get_billing_email();
213
		$billing_first_name = WC_Stripe_Helper::is_pre_30() ? $order->billing_first_name : $order->get_billing_first_name();
214
		$billing_last_name  = WC_Stripe_Helper::is_pre_30() ? $order->billing_last_name : $order->get_billing_last_name();
215
216
		if ( ! empty( $billing_email ) && apply_filters( 'wc_stripe_send_stripe_receipt', false ) ) {
217
			$post_data['receipt_email'] = $billing_email;
218
		}
219
220
		switch ( WC_Stripe_Helper::is_pre_30() ? $order->payment_method : $order->get_payment_method() ) {
221
			case 'stripe':
222
				if ( ! empty( $statement_descriptor ) ) {
223
					$post_data['statement_descriptor'] = WC_Stripe_Helper::clean_statement_descriptor( $statement_descriptor );
224
				}
225
226
				$post_data['capture'] = $capture ? 'true' : 'false';
227
				break;
228
			case 'stripe_sepa':
229
				if ( ! empty( $statement_descriptor ) ) {
230
					$post_data['statement_descriptor'] = WC_Stripe_Helper::clean_statement_descriptor( $statement_descriptor );
231
				}
232
				break;
233
		}
234
235
		$post_data['expand[]'] = 'balance_transaction';
236
237
		$metadata = array(
238
			__( 'customer_name', 'woocommerce-gateway-stripe' ) => sanitize_text_field( $billing_first_name ) . ' ' . sanitize_text_field( $billing_last_name ),
239
			__( 'customer_email', 'woocommerce-gateway-stripe' ) => sanitize_email( $billing_email ),
240
			'order_id' => WC_Stripe_Helper::is_pre_30() ? $order->id : $order->get_id(),
241
		);
242
243
		if ( $this->has_subscription( WC_Stripe_Helper::is_pre_30() ? $order->id : $order->get_id() ) ) {
244
			$metadata += array(
245
				'payment_type' => 'recurring',
246
				'site_url'     => esc_url( get_site_url() ),
247
			);
248
		}
249
250
		$post_data['metadata'] = apply_filters( 'wc_stripe_payment_metadata', $metadata, $order, $prepared_source );
251
252
		if ( $prepared_source->customer ) {
253
			$post_data['customer'] = $prepared_source->customer;
254
		}
255
256
		if ( $prepared_source->source ) {
257
			$post_data['source'] = $prepared_source->source;
258
		}
259
260
		/**
261
		 * Filter the return value of the WC_Payment_Gateway_CC::generate_payment_request.
262
		 *
263
		 * @since 3.1.0
264
		 * @param array $post_data
265
		 * @param WC_Order $order
266
		 * @param object $source
267
		 */
268
		return apply_filters( 'wc_stripe_generate_payment_request', $post_data, $order, $prepared_source );
269
	}
270
271
	/**
272
	 * Store extra meta data for an order from a Stripe Response.
273
	 */
274
	public function process_response( $response, $order ) {
275
		WC_Stripe_Logger::log( 'Processing response: ' . print_r( $response, true ) );
276
277
		$order_id = WC_Stripe_Helper::is_pre_30() ? $order->id : $order->get_id();
278
279
		$captured = ( isset( $response->captured ) && $response->captured ) ? 'yes' : 'no';
280
281
		// Store charge data.
282
		WC_Stripe_Helper::is_pre_30() ? update_post_meta( $order_id, '_stripe_charge_captured', $captured ) : $order->update_meta_data( '_stripe_charge_captured', $captured );
283
284
		// Store other data such as fees.
285 View Code Duplication
		if ( isset( $response->balance_transaction ) && isset( $response->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...
286
			// Fees and Net needs to both come from Stripe to be accurate as the returned
287
			// values are in the local currency of the Stripe account, not from WC.
288
			$fee = ! empty( $response->balance_transaction->fee ) ? WC_Stripe_Helper::format_balance_fee( $response->balance_transaction, 'fee' ) : 0;
289
			$net = ! empty( $response->balance_transaction->net ) ? WC_Stripe_Helper::format_balance_fee( $response->balance_transaction, 'net' ) : 0;
290
			WC_Stripe_Helper::is_pre_30() ? update_post_meta( $order_id, self::META_NAME_FEE, $fee ) : $order->update_meta_data( self::META_NAME_FEE, $fee );
291
			WC_Stripe_Helper::is_pre_30() ? update_post_meta( $order_id, self::META_NAME_NET, $net ) : $order->update_meta_data( self::META_NAME_NET, $net );
292
		}
293
294
		if ( 'yes' === $captured ) {
295
			/**
296
			 * Charge can be captured but in a pending state. Payment methods
297
			 * that are asynchronous may take couple days to clear. Webhook will
298
			 * take care of the status changes.
299
			 */
300
			if ( 'pending' === $response->status ) {
301
				$order_stock_reduced = WC_Stripe_Helper::is_pre_30() ? get_post_meta( $order_id, '_order_stock_reduced', true ) : $order->get_meta( '_order_stock_reduced', true );
302
303
				if ( ! $order_stock_reduced ) {
304
					WC_Stripe_Helper::is_pre_30() ? $order->reduce_order_stock() : wc_reduce_stock_levels( $order_id );
305
				}
306
307
				WC_Stripe_Helper::is_pre_30() ? update_post_meta( $order_id, '_transaction_id', $response->id ) : $order->set_transaction_id( $response->id );
308
				/* translators: transaction id */
309
				$order->update_status( 'on-hold', sprintf( __( 'Stripe charge awaiting payment: %s.', 'woocommerce-gateway-stripe' ), $response->id ) );
310
			}
311
312
			if ( 'succeeded' === $response->status ) {
313
				$order->payment_complete( $response->id );
314
315
				/* translators: transaction id */
316
				$message = sprintf( __( 'Stripe charge complete (Charge ID: %s)', 'woocommerce-gateway-stripe' ), $response->id );
317
				$order->add_order_note( $message );
318
			}
319
320
			if ( 'failed' === $response->status ) {
321
				$localized_message = __( 'Payment processing failed. Please retry.', 'woocommerce-gateway-stripe' );
322
				$order->add_order_note( $localized_message );
323
				throw new WC_Stripe_Exception( print_r( $response, true ), $localized_message );
324
			}
325
		} else {
326
			WC_Stripe_Helper::is_pre_30() ? update_post_meta( $order_id, '_transaction_id', $response->id ) : $order->set_transaction_id( $response->id );
327
328
			if ( $order->has_status( array( 'pending', 'failed' ) ) ) {
329
				WC_Stripe_Helper::is_pre_30() ? $order->reduce_order_stock() : wc_reduce_stock_levels( $order_id );
330
			}
331
332
			/* translators: transaction id */
333
			$order->update_status( 'on-hold', sprintf( __( 'Stripe charge authorized (Charge ID: %s). Process order to take payment, or cancel to remove the pre-authorization.', 'woocommerce-gateway-stripe' ), $response->id ) );
334
		}
335
336
		if ( is_callable( array( $order, 'save' ) ) ) {
337
			$order->save();
338
		}
339
340
		do_action( 'wc_gateway_stripe_process_response', $response, $order );
341
342
		return $response;
343
	}
344
345
	/**
346
	 * Sends the failed order email to admin.
347
	 *
348
	 * @since 3.1.0
349
	 * @version 4.0.0
350
	 * @param int $order_id
351
	 * @return null
352
	 */
353
	public function send_failed_order_email( $order_id ) {
354
		$emails = WC()->mailer()->get_emails();
355
		if ( ! empty( $emails ) && ! empty( $order_id ) ) {
356
			$emails['WC_Email_Failed_Order']->trigger( $order_id );
357
		}
358
	}
359
360
	/**
361
	 * Get owner details.
362
	 *
363
	 * @since 4.0.0
364
	 * @version 4.0.0
365
	 * @param object $order
366
	 * @return object $details
367
	 */
368
	public function get_owner_details( $order ) {
369
		$billing_first_name = WC_Stripe_Helper::is_pre_30() ? $order->billing_first_name : $order->get_billing_first_name();
370
		$billing_last_name  = WC_Stripe_Helper::is_pre_30() ? $order->billing_last_name : $order->get_billing_last_name();
371
372
		$details = array();
373
374
		$details['name']                   = $billing_first_name . ' ' . $billing_last_name;
375
		$details['email']                  = WC_Stripe_Helper::is_pre_30() ? $order->billing_email : $order->get_billing_email();
376
377
		$phone                             = WC_Stripe_Helper::is_pre_30() ? $order->billing_phone : $order->get_billing_phone();
378
379
		if ( ! empty( $phone ) ) {
380
			$details['phone']              = $phone;
381
		}
382
383
		$details['address']['line1']       = WC_Stripe_Helper::is_pre_30() ? $order->billing_address_1 : $order->get_billing_address_1();
384
		$details['address']['line2']       = WC_Stripe_Helper::is_pre_30() ? $order->billing_address_2 : $order->get_billing_address_2();
385
		$details['address']['state']       = WC_Stripe_Helper::is_pre_30() ? $order->billing_state : $order->get_billing_state();
386
		$details['address']['city']        = WC_Stripe_Helper::is_pre_30() ? $order->billing_city : $order->get_billing_city();
387
		$details['address']['postal_code'] = WC_Stripe_Helper::is_pre_30() ? $order->billing_postcode : $order->get_billing_postcode();
388
		$details['address']['country']     = WC_Stripe_Helper::is_pre_30() ? $order->billing_country : $order->get_billing_country();
389
390
		return (object) apply_filters( 'wc_stripe_owner_details', $details, $order );
391
	}
392
393
	/**
394
	 * Get source object by source id.
395
	 *
396
	 * @since 4.0.3
397
	 * @param string $source_id The source ID to get source object for.
398
	 */
399
	public function get_source_object( $source_id = '' ) {
400
		if ( empty( $source_id ) ) {
401
			return '';
402
		}
403
404
		$source_object = WC_Stripe_API::retrieve( 'sources/' . $source_id );
405
406
		if ( ! empty( $source_object->error ) ) {
407
			throw new WC_Stripe_Exception( print_r( $source_object, true ), $source_object->error->message );
408
		}
409
410
		return $source_object;
411
	}
412
413
	/**
414
	 * Checks if 3DS is required.
415
	 *
416
	 * @since 4.0.4
417
	 * @param object $source_object
418
	 * @return bool
419
	 */
420
	public function is_3ds_required( $source_object ) {
421
		return (
422
			$source_object && ! empty( $source_object->card ) ) &&
423
			( 'card' === $source_object->type && 'required' === $source_object->card->three_d_secure ||
424
			( $this->three_d_secure && 'optional' === $source_object->card->three_d_secure )
425
		);
426
	}
427
428
	/**
429
	 * Checks if card is 3DS.
430
	 *
431
	 * @since 4.0.4
432
	 * @param object $source_object
433
	 * @return bool
434
	 */
435
	public function is_3ds_card( $source_object ) {
436
		return ( $source_object && 'three_d_secure' === $source_object->type );
437
	}
438
439
	/**
440
	 * Checks if card is a prepaid card.
441
	 *
442
	 * @since 4.0.6
443
	 * @param object $source_object
444
	 * @return bool
445
	 */
446
	public function is_prepaid_card( $source_object ) {
447
		return ( $source_object && 'token' === $source_object->object && 'prepaid' === $source_object->card->funding );
448
	}
449
450
	/**
451
	 * Creates the 3DS source for charge.
452
	 *
453
	 * @since 4.0.0
454
	 * @since 4.0.4 Add $return_url
455
	 * @param object $order
456
	 * @param object $source_object
457
	 * @param string $return_url
458
	 * @return mixed
459
	 */
460
	public function create_3ds_source( $order, $source_object, $return_url = '' ) {
461
		$currency                    = WC_Stripe_Helper::is_pre_30() ? $order->get_order_currency() : $order->get_currency();
462
		$order_id                    = WC_Stripe_Helper::is_pre_30() ? $order->id : $order->get_id();
0 ignored issues
show
Unused Code introduced by
$order_id is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
463
		$return_url                  = empty( $return_url ) ? $this->get_stripe_return_url( $order ) : $return_url;
464
465
		$post_data                   = array();
466
		$post_data['amount']         = WC_Stripe_Helper::get_stripe_amount( $order->get_total(), $currency );
467
		$post_data['currency']       = strtolower( $currency );
468
		$post_data['type']           = 'three_d_secure';
469
		$post_data['owner']          = $this->get_owner_details( $order );
470
		$post_data['three_d_secure'] = array( 'card' => $source_object->id );
471
		$post_data['redirect']       = array( 'return_url' => $return_url );
472
473
		WC_Stripe_Logger::log( 'Info: Begin creating 3DS source...' );
474
475
		return WC_Stripe_API::request( apply_filters( 'wc_stripe_3ds_source', $post_data, $order ), 'sources' );
476
	}
477
478
	/**
479
	 * Get payment source. This can be a new token/source or existing WC token.
480
	 * If user is logged in and/or has WC account, create an account on Stripe.
481
	 * This way we can attribute the payment to the user to better fight fraud.
482
	 *
483
	 * @since 3.1.0
484
	 * @version 4.0.0
485
	 * @param string $user_id
486
	 * @param bool $force_save_source Should we force save payment source.
487
	 *
488
	 * @throws Exception When card was not added or for and invalid card.
489
	 * @return object
490
	 */
491
	public function prepare_source( $user_id, $force_save_source = false ) {
492
		$customer           = new WC_Stripe_Customer( $user_id );
493
		$set_customer       = true;
494
		$force_save_source  = apply_filters( 'wc_stripe_force_save_source', $force_save_source, $customer );
495
		$source_object      = '';
496
		$source_id          = '';
497
		$wc_token_id        = false;
498
		$payment_method     = isset( $_POST['payment_method'] ) ? wc_clean( $_POST['payment_method'] ) : 'stripe';
499
500
		// New CC info was entered and we have a new source to process.
501
		if ( ! empty( $_POST['stripe_source'] ) ) {
502
			$source_object = self::get_source_object( wc_clean( $_POST['stripe_source'] ) );
503
			$source_id     = $source_object->id;
504
505
			// This checks to see if customer opted to save the payment method to file.
506
			$maybe_saved_card = isset( $_POST[ 'wc-' . $payment_method . '-new-payment-method' ] ) && ! empty( $_POST[ 'wc-' . $payment_method . '-new-payment-method' ] );
507
508
			/**
509
			 * This is true if the user wants to store the card to their account.
510
			 * Criteria to save to file is they are logged in, they opted to save or product requirements and the source is
511
			 * actually reusable. Either that or force_save_source is true.
512
			 */
513
			if ( ( $user_id && $this->saved_cards && $maybe_saved_card && 'reusable' === $source_object->usage ) || $force_save_source ) {
514
				$response = $customer->add_source( $source_object->id );
515
516
				if ( ! empty( $response->error ) ) {
517
					throw new WC_Stripe_Exception( print_r( $response, true ), $response->error->message );
518
				}
519
			}
520
		} elseif ( isset( $_POST[ 'wc-' . $payment_method . '-payment-token' ] ) && 'new' !== $_POST[ 'wc-' . $payment_method . '-payment-token' ] ) {
521
			// Use an existing token, and then process the payment.
522
			$wc_token_id = wc_clean( $_POST[ 'wc-' . $payment_method . '-payment-token' ] );
523
			$wc_token    = WC_Payment_Tokens::get( $wc_token_id );
524
525
			if ( ! $wc_token || $wc_token->get_user_id() !== get_current_user_id() ) {
526
				WC()->session->set( 'refresh_totals', true );
527
				throw new WC_Stripe_Exception( 'Invalid payment method', __( 'Invalid payment method. Please input a new card number.', 'woocommerce-gateway-stripe' ) );
528
			}
529
530
			$source_id = $wc_token->get_token();
531
		} elseif ( isset( $_POST['stripe_token'] ) && 'new' !== $_POST['stripe_token'] ) {
532
			$stripe_token     = wc_clean( $_POST['stripe_token'] );
533
			$maybe_saved_card = isset( $_POST[ 'wc-' . $payment_method . '-new-payment-method' ] ) && ! empty( $_POST[ 'wc-' . $payment_method . '-new-payment-method' ] );
534
535
			// This is true if the user wants to store the card to their account.
536
			if ( ( $user_id && $this->saved_cards && $maybe_saved_card ) || $force_save_source ) {
537
				$response = $customer->add_source( $stripe_token );
538
539
				if ( ! empty( $response->error ) ) {
540
					throw new WC_Stripe_Exception( print_r( $response, true ), $response->error->message );
541
				}
542
			} else {
543
				$set_customer = false;
544
				$source_id    = $stripe_token;
545
			}
546
		}
547
548
		if ( ! $set_customer ) {
549
			$customer_id = false;
550
		} else {
551
			$customer_id = $customer->get_id() ? $customer->get_id() : false;
552
		}
553
554
		if ( empty( $source_object ) ) {
555
			$source_object = self::get_source_object( $source_id );
556
		}
557
558
		return (object) array(
559
			'token_id'      => $wc_token_id,
560
			'customer'      => $customer_id,
561
			'source'        => $source_id,
562
			'source_object' => $source_object,
563
		);
564
	}
565
566
	/**
567
	 * Get payment source from an order. This could be used in the future for
568
	 * a subscription as an example, therefore using the current user ID would
569
	 * not work - the customer won't be logged in :)
570
	 *
571
	 * Not using 2.6 tokens for this part since we need a customer AND a card
572
	 * token, and not just one.
573
	 *
574
	 * @since 3.1.0
575
	 * @version 4.0.0
576
	 * @param object $order
577
	 * @return object
578
	 */
579
	public function prepare_order_source( $order = null ) {
580
		$stripe_customer = new WC_Stripe_Customer();
581
		$stripe_source   = false;
582
		$token_id        = false;
583
584
		if ( $order ) {
585
			$order_id = WC_Stripe_Helper::is_pre_30() ? $order->id : $order->get_id();
586
587
			$stripe_customer_id = get_post_meta( $order_id, '_stripe_customer_id', true );
588
589
			if ( $stripe_customer_id ) {
590
				$stripe_customer->set_id( $stripe_customer_id );
591
			}
592
593
			$source_id = WC_Stripe_Helper::is_pre_30() ? get_post_meta( $order_id, '_stripe_source_id', true ) : $order->get_meta( '_stripe_source_id', true );
594
595
			// Since 4.0.0, we changed card to source so we need to account for that.
596
			if ( empty( $source_id ) ) {
597
				$source_id = WC_Stripe_Helper::is_pre_30() ? get_post_meta( $order_id, '_stripe_card_id', true ) : $order->get_meta( '_stripe_card_id', true );
598
599
				// Take this opportunity to update the key name.
600
				WC_Stripe_Helper::is_pre_30() ? update_post_meta( $order_id, '_stripe_source_id', $source_id ) : $order->update_meta_data( '_stripe_source_id', $source_id );
601
602
				if ( is_callable( array( $order, 'save' ) ) ) {
603
					$order->save();
604
				}
605
			}
606
607
			if ( $source_id ) {
608
				$stripe_source = $source_id;
609
			} elseif ( apply_filters( 'wc_stripe_use_default_customer_source', true ) ) {
610
				/*
611
				 * We can attempt to charge the customer's default source
612
				 * by sending empty source id.
613
				 */
614
				$stripe_source = '';
615
			}
616
		}
617
618
		return (object) array(
619
			'token_id' => $token_id,
620
			'customer' => $stripe_customer ? $stripe_customer->get_id() : false,
621
			'source'   => $stripe_source,
622
		);
623
	}
624
625
	/**
626
	 * Save source to order.
627
	 *
628
	 * @since 3.1.0
629
	 * @version 4.0.0
630
	 * @param WC_Order $order For to which the source applies.
631
	 * @param stdClass $source Source information.
632
	 */
633
	public function save_source_to_order( $order, $source ) {
634
		$order_id = WC_Stripe_Helper::is_pre_30() ? $order->id : $order->get_id();
635
636
		// Store source in the order.
637
		if ( $source->customer ) {
638
			if ( WC_Stripe_Helper::is_pre_30() ) {
639
				update_post_meta( $order_id, '_stripe_customer_id', $source->customer );
640
			} else {
641
				$order->update_meta_data( '_stripe_customer_id', $source->customer );
642
			}
643
		}
644
645
		if ( $source->source ) {
646
			if ( WC_Stripe_Helper::is_pre_30() ) {
647
				update_post_meta( $order_id, '_stripe_source_id', $source->source );
648
			} else {
649
				$order->update_meta_data( '_stripe_source_id', $source->source );
650
			}
651
		}
652
653
		if ( is_callable( array( $order, 'save' ) ) ) {
654
			$order->save();
655
		}
656
	}
657
658
	/**
659
	 * Updates Stripe fees/net.
660
	 * e.g usage would be after a refund.
661
	 *
662
	 * @since 4.0.0
663
	 * @version 4.0.6
664
	 * @param object $order The order object
665
	 * @param int $balance_transaction_id
666
	 */
667
	public function update_fees( $order, $balance_transaction_id ) {
668
		$order_id = WC_Stripe_Helper::is_pre_30() ? $order->id : $order->get_id();
669
670
		$balance_transaction = WC_Stripe_API::retrieve( 'balance/history/' . $balance_transaction_id );
671
672
		if ( empty( $balance_transaction->error ) ) {
673
			if ( isset( $balance_transaction ) && isset( $balance_transaction->fee ) ) {
674
				// Fees and Net needs to both come from Stripe to be accurate as the returned
675
				// values are in the local currency of the Stripe account, not from WC.
676
				$fee_refund = ! empty( $balance_transaction->fee ) ? WC_Stripe_Helper::format_balance_fee( $balance_transaction, 'fee' ) : 0;
677
				$net_refund = ! empty( $balance_transaction->net ) ? WC_Stripe_Helper::format_balance_fee( $balance_transaction, 'net' ) : 0;
678
679
				// Current data fee & net.
680
				$fee_current = WC_Stripe_Helper::is_pre_30() ? get_post_meta( $order_id, self::META_NAME_FEE, true ) : $order->get_meta( self::META_NAME_FEE, true );
681
				$net_current = WC_Stripe_Helper::is_pre_30() ? get_post_meta( $order_id, self::META_NAME_NET, true ) : $order->get_meta( self::META_NAME_NET, true );
682
683
				// Calculation.
684
				$fee = (float) $fee_current + (float) $fee_refund;
685
				$net = (float) $net_current + (float) $net_refund;
686
687
				WC_Stripe_Helper::is_pre_30() ? update_post_meta( $order_id, self::META_NAME_FEE, $fee ) : $order->update_meta_data( self::META_NAME_FEE, $fee );
688
				WC_Stripe_Helper::is_pre_30() ? update_post_meta( $order_id, self::META_NAME_NET, $net ) : $order->update_meta_data( self::META_NAME_NET, $net );
689
690
				if ( is_callable( array( $order, 'save' ) ) ) {
691
					$order->save();
692
				}
693
			}
694
		} else {
695
			WC_Stripe_Logger::log( "Unable to update fees/net meta for order: {$order_id}" );
696
		}
697
	}
698
699
	/**
700
	 * Refund a charge.
701
	 *
702
	 * @since 3.1.0
703
	 * @version 4.0.0
704
	 * @param  int $order_id
705
	 * @param  float $amount
706
	 * @return bool
707
	 */
708
	public function process_refund( $order_id, $amount = null, $reason = '' ) {
709
		$order = wc_get_order( $order_id );
710
711
		if ( ! $order || ! $order->get_transaction_id() ) {
712
			return false;
713
		}
714
715
		$request = array();
716
717
		if ( WC_Stripe_Helper::is_pre_30() ) {
718
			$order_currency = get_post_meta( $order_id, '_order_currency', true );
719
			$captured       = get_post_meta( $order_id, '_stripe_charge_captured', true );
720
		} else {
721
			$order_currency = $order->get_currency();
722
			$captured       = $order->get_meta( '_stripe_charge_captured', true );
723
		}
724
725
		if ( ! is_null( $amount ) ) {
726
			$request['amount'] = WC_Stripe_Helper::get_stripe_amount( $amount, $order_currency );
727
		}
728
729
		// If order is only authorized, don't pass amount.
730
		if ( 'yes' !== $captured ) {
731
			unset( $request['amount'] );
732
		}
733
734
		if ( $reason ) {
735
			$request['metadata'] = array(
736
				'reason' => $reason,
737
			);
738
		}
739
740
		$request['charge'] = $order->get_transaction_id();
741
742
		WC_Stripe_Logger::log( "Info: Beginning refund for order {$order->get_transaction_id()} for the amount of {$amount}" );
743
744
		$request = apply_filters( 'wc_stripe_refund_request', $request, $order );
745
746
		$response = WC_Stripe_API::request( $request, 'refunds' );
747
748
		if ( ! empty( $response->error ) ) {
749
			WC_Stripe_Logger::log( 'Error: ' . $response->error->message );
750
751
			return $response;
752
753
		} elseif ( ! empty( $response->id ) ) {
754
			WC_Stripe_Helper::is_pre_30() ? update_post_meta( $order_id, '_stripe_refund_id', $response->id ) : $order->update_meta_data( '_stripe_refund_id', $response->id );
755
756
			$amount = wc_price( $response->amount / 100 );
757
758
			if ( in_array( strtolower( $order->get_currency() ), WC_Stripe_Helper::no_decimal_currencies() ) ) {
759
				$amount = wc_price( $response->amount );
760
			}
761
762
			if ( isset( $response->balance_transaction ) ) {
763
				$this->update_fees( $order, $response->balance_transaction );
764
			}
765
766
			/* translators: 1) dollar amount 2) transaction id 3) refund message */
767
			$refund_message = ( isset( $captured ) && 'yes' === $captured ) ? sprintf( __( 'Refunded %1$s - Refund ID: %2$s - Reason: %3$s', 'woocommerce-gateway-stripe' ), $amount, $response->id, $reason ) : __( 'Pre-Authorization Released', 'woocommerce-gateway-stripe' );
768
769
			$order->add_order_note( $refund_message );
770
			WC_Stripe_Logger::log( 'Success: ' . html_entity_decode( strip_tags( $refund_message ) ) );
771
772
			return true;
773
		}
774
	}
775
776
	/**
777
	 * Add payment method via account screen.
778
	 * We don't store the token locally, but to the Stripe API.
779
	 *
780
	 * @since 3.0.0
781
	 * @version 4.0.0
782
	 */
783
	public function add_payment_method() {
784
		$error     = false;
785
		$error_msg = __( 'There was a problem adding the card.', 'woocommerce-gateway-stripe' );
786
		$source_id = '';
787
788
		if ( empty( $_POST['stripe_source'] ) && empty( $_POST['stripe_token'] ) || ! is_user_logged_in() ) {
789
			$error = true;
790
		}
791
792
		$stripe_customer = new WC_Stripe_Customer( get_current_user_id() );
793
794
		$source = ! empty( $_POST['stripe_source'] ) ? wc_clean( $_POST['stripe_source'] ) : '';
795
796
		$source_object = WC_Stripe_API::retrieve( 'sources/' . $source );
797
798
		if ( isset( $source_object ) ) {
799
			if ( ! empty( $source_object->error ) ) {
800
				$error = true;
801
			}
802
803
			$source_id = $source_object->id;
804
		} elseif ( isset( $_POST['stripe_token'] ) ) {
805
			$source_id = wc_clean( $_POST['stripe_token'] );
806
		}
807
808
		$response = $stripe_customer->add_source( $source_id );
809
810
		if ( ! $response || is_wp_error( $response ) || ! empty( $response->error ) ) {
811
			$error = true;
812
		}
813
814
		if ( $error ) {
815
			wc_add_notice( $error_msg, 'error' );
816
			WC_Stripe_Logger::log( 'Add payment method Error: ' . $error_msg );
817
			return;
818
		}
819
820
		return array(
821
			'result'   => 'success',
822
			'redirect' => wc_get_endpoint_url( 'payment-methods' ),
823
		);
824
	}
825
826
	/**
827
	 * Gets the locale with normalization that only Stripe accepts.
828
	 *
829
	 * @since 4.0.6
830
	 * @return string $locale
831
	 */
832
	public function get_locale() {
833
		$locale = get_locale();
834
835
		/*
836
		 * Stripe expects Norwegian to only be passed NO.
837
		 * But WP has different dialects.
838
		 */
839
		if ( 'NO' === substr( $locale, 3, 2 ) ) {
840
			$locale = 'no';
841
		} else {
842
			$locale = substr( get_locale(), 0, 2 );
843
		}
844
845
		return $locale;
846
	}
847
848
	/**
849
	 * Change the idempotency key so charge can
850
	 * process order as a different transaction.
851
	 *
852
	 * @since 4.0.6
853
	 * @param string $idempotency_key
854
	 * @param array $request
855
	 */
856
	public function change_idempotency_key( $idempotency_key, $request ) {
857
		$customer = ! empty( $request['customer'] ) ? $request['customer'] : '';
858
		$source   = ! empty( $request['source'] ) ? $request['source'] : $customer;
859
		$count    = $this->retry_interval;
860
861
		return $request['metadata']['order_id'] . '-' . $count . '-' . $source;
862
	}
863
864
	/**
865
	 * Checks if request is the original to prevent double processing
866
	 * on WC side. The original-request header and request-id header
867
	 * needs to be the same to mean its the original request.
868
	 *
869
	 * @since 4.0.6
870
	 * @param array $headers
871
	 */
872
	public function is_original_request( $headers ) {
873
		if ( $headers['original-request'] === $headers['request-id'] ) {
874
			return true;
875
		}
876
877
		return false;
878
	}
879
}
880