Completed
Pull Request — master (#1398)
by Radoslav
02:05
created

WC_Stripe_Customer::add_source()   B

Complexity

Conditions 9
Paths 12

Size

Total Lines 54

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 9
nc 12
nop 1
dl 0
loc 54
rs 7.448
c 0
b 0
f 0

How to fix   Long Method   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
if ( ! defined( 'ABSPATH' ) ) {
3
	exit;
4
}
5
6
/**
7
 * WC_Stripe_Customer class.
8
 *
9
 * Represents a Stripe Customer.
10
 */
11
class WC_Stripe_Customer {
12
13
	/**
14
	 * Stripe customer ID
15
	 * @var string
16
	 */
17
	private $id = '';
18
19
	/**
20
	 * WP User ID
21
	 * @var integer
22
	 */
23
	private $user_id = 0;
24
25
	/**
26
	 * Data from API
27
	 * @var array
28
	 */
29
	private $customer_data = array();
30
31
	/**
32
	 * Constructor
33
	 * @param int $user_id The WP user ID
34
	 */
35
	public function __construct( $user_id = 0 ) {
36
		if ( $user_id ) {
37
			$this->set_user_id( $user_id );
38
			$this->set_id( $this->get_id_from_meta( $user_id ) );
39
		}
40
	}
41
42
	/**
43
	 * Get Stripe customer ID.
44
	 * @return string
45
	 */
46
	public function get_id() {
47
		return $this->id;
48
	}
49
50
	/**
51
	 * Set Stripe customer ID.
52
	 * @param [type] $id [description]
0 ignored issues
show
Documentation introduced by
The doc-type [type] could not be parsed: Unknown type name "" at position 0. [(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...
53
	 */
54
	public function set_id( $id ) {
55
		// Backwards compat for customer ID stored in array format. (Pre 3.0)
56
		if ( is_array( $id ) && isset( $id['customer_id'] ) ) {
57
			$id = $id['customer_id'];
58
59
			$this->update_id_in_meta( $id );
60
		}
61
62
		$this->id = wc_clean( $id );
63
	}
64
65
	/**
66
	 * User ID in WordPress.
67
	 * @return int
68
	 */
69
	public function get_user_id() {
70
		return absint( $this->user_id );
71
	}
72
73
	/**
74
	 * Set User ID used by WordPress.
75
	 * @param int $user_id
76
	 */
77
	public function set_user_id( $user_id ) {
78
		$this->user_id = absint( $user_id );
79
	}
80
81
	/**
82
	 * Get user object.
83
	 * @return WP_User
84
	 */
85
	protected function get_user() {
86
		return $this->get_user_id() ? get_user_by( 'id', $this->get_user_id() ) : false;
87
	}
88
89
	/**
90
	 * Store data from the Stripe API about this customer
91
	 */
92
	public function set_customer_data( $data ) {
93
		$this->customer_data = $data;
94
	}
95
96
	/**
97
	 * Generates the customer request, used for both creating and updating customers.
98
	 *
99
	 * @param  array $args Additional arguments (optional).
100
	 * @return array
101
	 */
102
	protected function generate_customer_request( $args = array() ) {
103
		$billing_email = isset( $_POST['billing_email'] ) ? filter_var( $_POST['billing_email'], FILTER_SANITIZE_EMAIL ) : '';
104
		$user          = $this->get_user();
105
106
		if ( $user ) {
107
			$billing_first_name = get_user_meta( $user->ID, 'billing_first_name', true );
108
			$billing_last_name  = get_user_meta( $user->ID, 'billing_last_name', true );
109
110
			// If billing first name does not exists try the user first name.
111
			if ( empty( $billing_first_name ) ) {
112
				$billing_first_name = get_user_meta( $user->ID, 'first_name', true );
113
			}
114
115
			// If billing last name does not exists try the user last name.
116
			if ( empty( $billing_last_name ) ) {
117
				$billing_last_name = get_user_meta( $user->ID, 'last_name', true );
118
			}
119
120
			// translators: %1$s First name, %2$s Second name, %3$s Username.
121
			$description = sprintf( __( 'Name: %1$s %2$s, Username: %s', 'woocommerce-gateway-stripe' ), $billing_first_name, $billing_last_name, $user->user_login );
122
123
			$defaults = array(
124
				'email'       => $user->user_email,
125
				'description' => $description,
126
			);
127
128
			$billing_full_name = trim( $billing_first_name . ' ' . $billing_last_name );
129
			if ( ! empty( $billing_full_name ) ) {
130
				$defaults['name'] = $billing_full_name;
131
			}
132
		} else {
133
			$billing_first_name = isset( $_POST['billing_first_name'] ) ? filter_var( wp_unslash( $_POST['billing_first_name'] ), FILTER_SANITIZE_STRING ) : ''; // phpcs:ignore WordPress.Security.NonceVerification
134
			$billing_last_name  = isset( $_POST['billing_last_name'] ) ? filter_var( wp_unslash( $_POST['billing_last_name'] ), FILTER_SANITIZE_STRING ) : ''; // phpcs:ignore WordPress.Security.NonceVerification
135
136
			// translators: %1$s First name, %2$s Second name.
137
			$description = sprintf( __( 'Name: %1$s %2$s, Guest', 'woocommerce-gateway-stripe' ), $billing_first_name, $billing_last_name );
138
139
			$defaults = array(
140
				'email'       => $billing_email,
141
				'description' => $description,
142
			);
143
144
			$billing_full_name = trim( $billing_first_name . ' ' . $billing_last_name );
145
			if ( ! empty( $billing_full_name ) ) {
146
				$defaults['name'] = $billing_full_name;
147
			}
148
		}
149
150
		$metadata             = array();
151
		$defaults['metadata'] = apply_filters( 'wc_stripe_customer_metadata', $metadata, $user );
152
153
		return wp_parse_args( $args, $defaults );
154
	}
155
156
	/**
157
	 * Create a customer via API.
158
	 * @param array $args
159
	 * @return WP_Error|int
160
	 */
161
	public function create_customer( $args = array() ) {
162
		$args     = $this->generate_customer_request( $args );
163
		$response = WC_Stripe_API::request( apply_filters( 'wc_stripe_create_customer_args', $args ), 'customers' );
164
165 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...
166
			throw new WC_Stripe_Exception( print_r( $response, true ), $response->error->message );
167
		}
168
169
		$this->set_id( $response->id );
170
		$this->clear_cache();
171
		$this->set_customer_data( $response );
172
173
		if ( $this->get_user_id() ) {
174
			$this->update_id_in_meta( $response->id );
175
		}
176
177
		do_action( 'woocommerce_stripe_add_customer', $args, $response );
178
179
		return $response->id;
180
	}
181
182
	/**
183
	 * Updates the Stripe customer through the API.
184
	 *
185
	 * @param array $args     Additional arguments for the request (optional).
186
	 * @param bool  $is_retry Whether the current call is a retry (optional, defaults to false). If true, then an exception will be thrown instead of further retries on error.
187
	 *
188
	 * @return string Customer ID
189
	 *
190
	 * @throws WC_Stripe_Exception
191
	 */
192
	public function update_customer( $args = array(), $is_retry = false ) {
193
		if ( empty( $this->get_id() ) ) {
194
			throw new WC_Stripe_Exception( 'id_required_to_update_user', __( 'Attempting to update a Stripe customer without a customer ID.', 'woocommerce-gateway-stripe' ) );
195
		}
196
197
		$args     = $this->generate_customer_request( $args );
198
		$args     = apply_filters( 'wc_stripe_update_customer_args', $args );
199
		$response = WC_Stripe_API::request( $args, 'customers/' . $this->get_id() );
200
201
		if ( ! empty( $response->error ) ) {
202
			if ( $this->is_no_such_customer_error( $response->error ) && ! $is_retry ) {
203
				// This can happen when switching the main Stripe account or importing users from another site.
204
				// If not already retrying, recreate the customer and then try updating it again.
205
				$this->recreate_customer();
206
				return $this->update_customer( $args, true );
207
			}
208
209
			throw new WC_Stripe_Exception( print_r( $response, true ), $response->error->message );
210
		}
211
212
		$this->clear_cache();
213
		$this->set_customer_data( $response );
214
215
		do_action( 'woocommerce_stripe_update_customer', $args, $response );
216
217
		return $this->get_id();
218
	}
219
220
	/**
221
	 * Checks to see if error is of invalid request
222
	 * error and it is no such customer.
223
	 *
224
	 * @since 4.1.2
225
	 * @param array $error
226
	 */
227
	public function is_no_such_customer_error( $error ) {
228
		return (
229
			$error &&
0 ignored issues
show
Bug Best Practice introduced by
The expression $error of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
230
			'invalid_request_error' === $error->type &&
231
			preg_match( '/No such customer/i', $error->message )
232
		);
233
	}
234
235
	/**
236
	 * Checks to see if error is of invalid request
237
	 * error and it is no such customer.
238
	 *
239
	 * @since 4.5.6
240
	 * @param array $error
241
	 * @return bool
242
	 */
243
	public function is_source_already_attached_error( $error ) {
244
		return (
245
			$error &&
0 ignored issues
show
Bug Best Practice introduced by
The expression $error of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
246
			'invalid_request_error' === $error->type &&
247
			preg_match( '/already been attached to a customer/i', $error->message )
248
		);
249
	}
250
251
	/**
252
	 * Add a source for this stripe customer.
253
	 * @param string $source_id
254
	 * @return WP_Error|int
255
	 */
256
	public function add_source( $source_id ) {
257
		if ( ! $this->get_id() ) {
258
			$this->set_id( $this->create_customer() );
259
		}
260
261
		$response = $this->attach_source( $source_id );
262
263
		// Add token to WooCommerce.
264
		$wc_token = false;
265
266
		if ( $this->get_user_id() && class_exists( 'WC_Payment_Token_CC' ) ) {
267
			if ( ! empty( $response->type ) ) {
268
				switch ( $response->type ) {
269
					case 'alipay':
270
						break;
271
					case 'sepa_debit':
272
						$wc_token = new WC_Payment_Token_SEPA();
273
						$wc_token->set_token( $response->id );
274
						$wc_token->set_gateway_id( 'stripe_sepa' );
275
						$wc_token->set_last4( $response->sepa_debit->last4 );
276
						break;
277
					default:
278
						if ( 'source' === $response->object && 'card' === $response->type ) {
279
							$wc_token = new WC_Payment_Token_CC();
280
							$wc_token->set_token( $response->id );
281
							$wc_token->set_gateway_id( 'stripe' );
282
							$wc_token->set_card_type( strtolower( $response->card->brand ) );
283
							$wc_token->set_last4( $response->card->last4 );
284
							$wc_token->set_expiry_month( $response->card->exp_month );
285
							$wc_token->set_expiry_year( $response->card->exp_year );
286
						}
287
						break;
288
				}
289
			} else {
290
				// Legacy.
291
				$wc_token = new WC_Payment_Token_CC();
292
				$wc_token->set_token( $response->id );
293
				$wc_token->set_gateway_id( 'stripe' );
294
				$wc_token->set_card_type( strtolower( $response->brand ) );
295
				$wc_token->set_last4( $response->last4 );
296
				$wc_token->set_expiry_month( $response->exp_month );
297
				$wc_token->set_expiry_year( $response->exp_year );
298
			}
299
300
			$wc_token->set_user_id( $this->get_user_id() );
301
			$wc_token->save();
302
		}
303
304
		$this->clear_cache();
305
306
		do_action( 'woocommerce_stripe_add_source', $this->get_id(), $wc_token, $response, $source_id );
307
308
		return $response->id;
309
	}
310
311
	/**
312
	 * Attaches a source to the Stripe customer.
313
	 *
314
	 * @param string $source_id The ID of the new source.
315
	 * @return object|WP_Error Either a source object, or a WP error.
316
	 */
317
	public function attach_source( $source_id ) {
318
		$response = WC_Stripe_API::request(
319
			array(
320
				'source' => $source_id,
321
			),
322
			'customers/' . $this->get_id() . '/sources'
323
		);
324
325
		if ( ! empty( $response->error ) ) {
326
			// It is possible the WC user once was linked to a customer on Stripe
327
			// but no longer exists. Instead of failing, lets try to create a
328
			// new customer.
329
			if ( $this->is_no_such_customer_error( $response->error ) ) {
330
				$this->recreate_customer();
331
				return $this->add_source( $source_id );
332
			} elseif( $this->is_source_already_attached_error( $response->error ) ) {
333
				return WC_Stripe_API::request( array(), 'sources/' . $source_id, 'GET' );
334
			} else {
335
				return $response;
336
			}
337
		} elseif ( empty( $response->id ) ) {
338
			return new WP_Error( 'error', __( 'Unable to add payment source.', 'woocommerce-gateway-stripe' ) );
339
		} else {
340
			return $response;
341
		}
342
	}
343
344
	/**
345
	 * Get a customers saved sources using their Stripe ID.
346
	 *
347
	 * @param  string $customer_id
0 ignored issues
show
Bug introduced by
There is no parameter named $customer_id. Was it maybe removed?

This check looks for PHPDoc comments describing methods or function parameters that do not exist on the corresponding method or function.

Consider the following example. The parameter $italy is not defined by the method finale(...).

/**
 * @param array $germany
 * @param array $island
 * @param array $italy
 */
function finale($germany, $island) {
    return "2:1";
}

The most likely cause is that the parameter was removed, but the annotation was not.

Loading history...
348
	 * @return array
349
	 */
350
	public function get_sources() {
351
		if ( ! $this->get_id() ) {
352
			return array();
353
		}
354
355
		$sources = get_transient( 'stripe_sources_' . $this->get_id() );
356
357
		if ( false === $sources ) {
358
			$response = WC_Stripe_API::request(
359
				array(
360
					'limit' => 100,
361
				),
362
				'customers/' . $this->get_id() . '/sources',
363
				'GET'
364
			);
365
366
			if ( ! empty( $response->error ) ) {
367
				return array();
368
			}
369
370
			if ( is_array( $response->data ) ) {
371
				$sources = $response->data;
372
			}
373
374
			set_transient( 'stripe_sources_' . $this->get_id(), $sources, DAY_IN_SECONDS );
375
		}
376
377
		return empty( $sources ) ? array() : $sources;
378
	}
379
380
	/**
381
	 * Delete a source from stripe.
382
	 * @param string $source_id
383
	 */
384
	public function delete_source( $source_id ) {
385
		if ( ! $this->get_id() ) {
386
			return false;
387
		}
388
389
		$response = WC_Stripe_API::request( array(), 'customers/' . $this->get_id() . '/sources/' . sanitize_text_field( $source_id ), 'DELETE' );
390
391
		$this->clear_cache();
392
393
		if ( empty( $response->error ) ) {
394
			do_action( 'wc_stripe_delete_source', $this->get_id(), $response );
395
396
			return true;
397
		}
398
399
		return false;
400
	}
401
402
	/**
403
	 * Set default source in Stripe
404
	 * @param string $source_id
405
	 */
406
	public function set_default_source( $source_id ) {
407
		$response = WC_Stripe_API::request(
408
			array(
409
				'default_source' => sanitize_text_field( $source_id ),
410
			),
411
			'customers/' . $this->get_id(),
412
			'POST'
413
		);
414
415
		$this->clear_cache();
416
417
		if ( empty( $response->error ) ) {
418
			do_action( 'wc_stripe_set_default_source', $this->get_id(), $response );
419
420
			return true;
421
		}
422
423
		return false;
424
	}
425
426
	/**
427
	 * Deletes caches for this users cards.
428
	 */
429
	public function clear_cache() {
430
		delete_transient( 'stripe_sources_' . $this->get_id() );
431
		delete_transient( 'stripe_customer_' . $this->get_id() );
432
		$this->customer_data = array();
433
	}
434
435
	/**
436
	 * Retrieves the Stripe Customer ID from the user meta.
437
	 *
438
	 * @param  int $user_id The ID of the WordPress user.
439
	 * @return string|bool  Either the Stripe ID or false.
440
	 */
441
	public function get_id_from_meta( $user_id ) {
442
		return get_user_option( '_stripe_customer_id', $user_id );
443
	}
444
445
	/**
446
	 * Updates the current user with the right Stripe ID in the meta table.
447
	 *
448
	 * @param string $id The Stripe customer ID.
449
	 */
450
	public function update_id_in_meta( $id ) {
451
		update_user_option( $this->get_user_id(), '_stripe_customer_id', $id, false );
452
	}
453
454
	/**
455
	 * Deletes the user ID from the meta table with the right key.
456
	 */
457
	public function delete_id_from_meta() {
458
		delete_user_option( $this->get_user_id(), '_stripe_customer_id', false );
459
	}
460
461
	/**
462
	 * Recreates the customer for this user.
463
	 *
464
	 * @return string ID of the new Customer object.
465
	 */
466
	private function recreate_customer() {
467
		$this->delete_id_from_meta();
468
		return $this->create_customer();
469
	}
470
}
471