Completed
Push — master ( a519f9...82501a )
by Roy
10s
created

process_subscription_payment()   C

Complexity

Conditions 9
Paths 15

Size

Total Lines 50
Code Lines 28

Duplication

Lines 3
Ratio 6 %

Importance

Changes 0
Metric Value
dl 3
loc 50
rs 6
c 0
b 0
f 0
cc 9
eloc 28
nc 15
nop 2
1
<?php
2
if ( ! defined( 'ABSPATH' ) ) {
3
	exit;
4
}
5
6
/**
7
 * WC_Gateway_Stripe_Addons class.
8
 *
9
 * @extends WC_Gateway_Stripe
10
 */
11
class WC_Gateway_Stripe_Addons extends WC_Gateway_Stripe {
12
13
	public $wc_pre_30;
14
15
	/**
16
	 * Constructor
17
	 */
18
	public function __construct() {
19
		parent::__construct();
20
21
		if ( class_exists( 'WC_Subscriptions_Order' ) ) {
22
			add_action( 'woocommerce_scheduled_subscription_payment_' . $this->id, array( $this, 'scheduled_subscription_payment' ), 10, 2 );
23
			add_action( 'wcs_resubscribe_order_created', array( $this, 'delete_resubscribe_meta' ), 10 );
24
			add_action( 'wcs_renewal_order_created', array( $this, 'delete_renewal_meta' ), 10 );
25
			add_action( 'woocommerce_subscription_failing_payment_method_updated_stripe', array( $this, 'update_failing_payment_method' ), 10, 2 );
26
27
			// display the credit card used for a subscription in the "My Subscriptions" table
28
			add_filter( 'woocommerce_my_subscriptions_payment_method', array( $this, 'maybe_render_subscription_payment_method' ), 10, 2 );
29
30
			// allow store managers to manually set Stripe as the payment method on a subscription
31
			add_filter( 'woocommerce_subscription_payment_meta', array( $this, 'add_subscription_payment_meta' ), 10, 2 );
32
			add_filter( 'woocommerce_subscription_validate_payment_meta', array( $this, 'validate_subscription_payment_meta' ), 10, 2 );
33
		}
34
35
		if ( class_exists( 'WC_Pre_Orders_Order' ) ) {
36
			add_action( 'wc_pre_orders_process_pre_order_completion_payment_' . $this->id, array( $this, 'process_pre_order_release_payment' ) );
37
		}
38
39
		$this->wc_pre_30 = version_compare( WC_VERSION, '3.0.0', '<' );
40
	}
41
42
	/**
43
	 * Is $order_id a subscription?
44
	 * @param  int  $order_id
45
	 * @return boolean
46
	 */
47
	protected function is_subscription( $order_id ) {
48
		return ( function_exists( 'wcs_order_contains_subscription' ) && ( wcs_order_contains_subscription( $order_id ) || wcs_is_subscription( $order_id ) || wcs_order_contains_renewal( $order_id ) ) );
49
	}
50
51
	/**
52
	 * Is $order_id a pre-order?
53
	 * @param  int  $order_id
54
	 * @return boolean
55
	 */
56
	protected function is_pre_order( $order_id ) {
57
		return ( class_exists( 'WC_Pre_Orders_Order' ) && WC_Pre_Orders_Order::order_contains_pre_order( $order_id ) );
58
	}
59
60
	/**
61
	 * Process the payment based on type.
62
	 * @param  int $order_id
63
	 * @return array
64
	 */
65
	public function process_payment( $order_id, $retry = true, $force_customer = false ) {
66
		if ( $this->is_subscription( $order_id ) ) {
67
			// Regular payment with force customer enabled
68
			return parent::process_payment( $order_id, true, true );
69
70
		} elseif ( $this->is_pre_order( $order_id ) ) {
71
			return $this->process_pre_order( $order_id, $retry, $force_customer );
72
73
		} else {
74
			return parent::process_payment( $order_id, $retry, $force_customer );
75
		}
76
	}
77
78
	/**
79
	 * Updates other subscription sources.
80
	 */
81
	protected function save_source( $order, $source ) {
82
		parent::save_source( $order, $source );
83
84
		$order_id  = $this->wc_pre_30 ? $order->id : $order->get_id();
85
86
		// Also store it on the subscriptions being purchased or paid for in the order
87
		if ( function_exists( 'wcs_order_contains_subscription' ) && wcs_order_contains_subscription( $order_id ) ) {
88
			$subscriptions = wcs_get_subscriptions_for_order( $order_id );
89
		} elseif ( function_exists( 'wcs_order_contains_renewal' ) && wcs_order_contains_renewal( $order_id ) ) {
90
			$subscriptions = wcs_get_subscriptions_for_renewal_order( $order_id );
91
		} else {
92
			$subscriptions = array();
93
		}
94
95
		foreach ( $subscriptions as $subscription ) {
96
			$subscription_id = $this->wc_pre_30 ? $subscription->id : $subscription->get_id();
97
			update_post_meta( $subscription_id, '_stripe_customer_id', $source->customer );
98
			update_post_meta( $subscription_id, '_stripe_card_id', $source->source );
99
		}
100
	}
101
102
	/**
103
	 * process_subscription_payment function.
104
	 * @param mixed $order
105
	 * @param int $amount (default: 0)
106
	 * @param string $stripe_token (default: '')
0 ignored issues
show
Bug introduced by
There is no parameter named $stripe_token. 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...
107
	 * @param  bool initial_payment
108
	 */
109
	public function process_subscription_payment( $order = '', $amount = 0 ) {
110 View Code Duplication
		if ( $amount * 100 < WC_Stripe::get_minimum_amount() ) {
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...
111
			return new WP_Error( 'stripe_error', sprintf( __( 'Sorry, the minimum allowed order total is %1$s to use this payment method.', 'woocommerce-gateway-stripe' ), wc_price( WC_Stripe::get_minimum_amount() / 100 ) ) );
112
		}
113
114
		// Get source from order
115
		$source = $this->get_order_source( $order );
116
117
		// If no order source was defined, use user source instead.
118
		if ( ! $source->customer ) {
119
			$source = $this->get_source( ( $this->wc_pre_30 ? $order->customer_user : $order->get_customer_id() ) );
120
		}
121
122
		// Or fail :(
0 ignored issues
show
Unused Code Comprehensibility introduced by
43% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
123
		if ( ! $source->customer ) {
124
			return new WP_Error( 'stripe_error', __( 'Customer not found', 'woocommerce-gateway-stripe' ) );
125
		}
126
127
		$order_id = $this->wc_pre_30 ? $order->id : $order->get_id();
128
		$this->log( "Info: Begin processing subscription payment for order {$order_id} for the amount of {$amount}" );
129
130
		// Make the request
131
		$request             = $this->generate_payment_request( $order, $source );
132
		$request['capture']  = 'true';
133
		$request['amount']   = $this->get_stripe_amount( $amount, $request['currency'] );
134
		$request['metadata'] = array(
135
			'payment_type'   => 'recurring',
136
			'site_url'       => esc_url( get_site_url() ),
137
		);
138
		$response            = WC_Stripe_API::request( $request );
139
140
		// Process valid response
141
		if ( is_wp_error( $response ) ) {
142
			if ( 'missing' === $response->get_error_code() ) {
143
				// If we can't link customer to a card, we try to charge by customer ID.
144
				$request             = $this->generate_payment_request( $order, $this->get_source( ( $this->wc_pre_30 ? $order->customer_user : $order->get_customer_id() ) ) );
145
				$request['capture']  = 'true';
146
				$request['amount']   = $this->get_stripe_amount( $amount, $request['currency'] );
147
				$request['metadata'] = array(
148
					'payment_type'   => 'recurring',
149
					'site_url'       => esc_url( get_site_url() ),
150
				);
151
				$response          = WC_Stripe_API::request( $request );
152
			}
153
		}
154
155
		$this->process_response( $response, $order );
156
157
		return $response;
158
	}
159
160
	/**
161
	 * Process the pre-order
162
	 * @param int $order_id
163
	 * @return array
164
	 */
165
	public function process_pre_order( $order_id, $retry, $force_customer ) {
166
		if ( WC_Pre_Orders_Order::order_requires_payment_tokenization( $order_id ) ) {
167
			try {
168
				$order = wc_get_order( $order_id );
169
170 View Code Duplication
				if ( $order->get_total() * 100 < WC_Stripe::get_minimum_amount() ) {
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...
171
					throw new Exception( sprintf( __( 'Sorry, the minimum allowed order total is %1$s to use this payment method.', 'woocommerce-gateway-stripe' ), wc_price( WC_Stripe::get_minimum_amount() / 100 ) ) );
172
				}
173
174
				$source = $this->get_source( get_current_user_id(), true );
175
176
				// We need a source on file to continue.
177
				if ( empty( $source->customer ) || empty( $source->source ) ) {
178
					throw new Exception( __( 'Unable to store payment details. Please try again.', 'woocommerce-gateway-stripe' ) );
179
				}
180
181
				// Store source to order meta
182
				$this->save_source( $order, $source );
183
184
				// Remove cart
185
				WC()->cart->empty_cart();
186
187
				// Is pre ordered!
188
				WC_Pre_Orders_Order::mark_order_as_pre_ordered( $order );
189
190
				// Return thank you page redirect
191
				return array(
192
					'result'   => 'success',
193
					'redirect' => $this->get_return_url( $order ),
194
				);
195
			} catch ( Exception $e ) {
196
				wc_add_notice( $e->getMessage(), 'error' );
197
				return;
198
			}
199
		} else {
200
			return parent::process_payment( $order_id, $retry, $force_customer );
0 ignored issues
show
Comprehensibility Bug introduced by
It seems like you call parent on a different method (process_payment() instead of process_pre_order()). Are you sure this is correct? If so, you might want to change this to $this->process_payment().

This check looks for a call to a parent method whose name is different than the method from which it is called.

Consider the following code:

class Daddy
{
    protected function getFirstName()
    {
        return "Eidur";
    }

    protected function getSurName()
    {
        return "Gudjohnsen";
    }
}

class Son
{
    public function getFirstName()
    {
        return parent::getSurname();
    }
}

The getFirstName() method in the Son calls the wrong method in the parent class.

Loading history...
201
		}
202
	}
203
204
	/**
205
	 * Process a pre-order payment when the pre-order is released
206
	 * @param WC_Order $order
207
	 * @return void
208
	 */
209
	public function process_pre_order_release_payment( $order ) {
210
		try {
211
			// Define some callbacks if the first attempt fails.
212
			$retry_callbacks = array(
213
				'remove_order_source_before_retry',
214
				'remove_order_customer_before_retry',
215
			);
216
217
			while ( 1 ) {
218
				$source   = $this->get_order_source( $order );
219
				$response = WC_Stripe_API::request( $this->generate_payment_request( $order, $source ) );
220
221
				if ( is_wp_error( $response ) ) {
222
					if ( 0 === sizeof( $retry_callbacks ) ) {
223
						throw new Exception( $response->get_error_message() );
224
					} else {
225
						$retry_callback = array_shift( $retry_callbacks );
226
						call_user_func( array( $this, $retry_callback ), $order );
227
					}
228
				} else {
229
					// Successful
230
					$this->process_response( $response, $order );
231
					break;
232
				}
233
			}
234
		} catch ( Exception $e ) {
235
			$order_note = sprintf( __( 'Stripe Transaction Failed (%s)', 'woocommerce-gateway-stripe' ), $e->getMessage() );
236
237
			// Mark order as failed if not already set,
238
			// otherwise, make sure we add the order note so we can detect when someone fails to check out multiple times
239
			if ( ! $order->has_status( 'failed' ) ) {
240
				$order->update_status( 'failed', $order_note );
241
			} else {
242
				$order->add_order_note( $order_note );
243
			}
244
		}
245
	}
246
247
	/**
248
	 * Don't transfer Stripe customer/token meta to resubscribe orders.
249
	 * @param int $resubscribe_order The order created for the customer to resubscribe to the old expired/cancelled subscription
250
	 */
251
	public function delete_resubscribe_meta( $resubscribe_order ) {
252
		delete_post_meta( ( $this->wc_pre_30 ? $resubscribe_order->id : $resubscribe_order->get_id() ), '_stripe_customer_id' );
0 ignored issues
show
Bug introduced by
The method get_id cannot be called on $resubscribe_order (of type integer).

Methods can only be called on objects. This check looks for methods being called on variables that have been inferred to never be objects.

Loading history...
253
		delete_post_meta( ( $this->wc_pre_30 ? $resubscribe_order->id : $resubscribe_order->get_id() ), '_stripe_card_id' );
0 ignored issues
show
Bug introduced by
The method get_id cannot be called on $resubscribe_order (of type integer).

Methods can only be called on objects. This check looks for methods being called on variables that have been inferred to never be objects.

Loading history...
254
		$this->delete_renewal_meta( $resubscribe_order );
255
	}
256
257
	/**
258
	 * Don't transfer Stripe fee/ID meta to renewal orders.
259
	 * @param int $resubscribe_order The order created for the customer to resubscribe to the old expired/cancelled subscription
0 ignored issues
show
Bug introduced by
There is no parameter named $resubscribe_order. 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...
260
	 */
261
	public function delete_renewal_meta( $renewal_order ) {
262
		delete_post_meta( ( $this->wc_pre_30 ? $renewal_order->id : $renewal_order->get_id() ), 'Stripe Fee' );
263
		delete_post_meta( ( $this->wc_pre_30 ? $renewal_order->id : $renewal_order->get_id() ), 'Net Revenue From Stripe' );
264
		delete_post_meta( ( $this->wc_pre_30 ? $renewal_order->id : $renewal_order->get_id() ), 'Stripe Payment ID' );
265
		return $renewal_order;
266
	}
267
268
	/**
269
	 * scheduled_subscription_payment function.
270
	 *
271
	 * @param $amount_to_charge float The amount to charge.
272
	 * @param $renewal_order WC_Order A WC_Order object created to record the renewal payment.
273
	 */
274
	public function scheduled_subscription_payment( $amount_to_charge, $renewal_order ) {
275
		$response = $this->process_subscription_payment( $renewal_order, $amount_to_charge );
276
277
		if ( is_wp_error( $response ) ) {
278
			$renewal_order->update_status( 'failed', sprintf( __( 'Stripe Transaction Failed (%s)', 'woocommerce-gateway-stripe' ), $response->get_error_message() ) );
279
		}
280
	}
281
282
	/**
283
	 * Remove order meta
284
	 * @param  object $order
285
	 */
286
	public function remove_order_source_before_retry( $order ) {
287
		$order_id = $this->wc_pre_30 ? $order->id : $order->get_id();
288
		delete_post_meta( $order_id, '_stripe_card_id' );
289
	}
290
291
	/**
292
	 * Remove order meta
293
	 * @param  object $order
294
	 */
295
	public function remove_order_customer_before_retry( $order ) {
296
		$order_id = $this->wc_pre_30 ? $order->id : $order->get_id();
297
		delete_post_meta( $order_id, '_stripe_customer_id' );
298
	}
299
300
	/**
301
	 * Update the customer_id for a subscription after using Stripe to complete a payment to make up for
302
	 * an automatic renewal payment which previously failed.
303
	 *
304
	 * @access public
305
	 * @param WC_Subscription $subscription The subscription for which the failing payment method relates.
306
	 * @param WC_Order $renewal_order The order which recorded the successful payment (to make up for the failed automatic payment).
307
	 * @return void
308
	 */
309
	public function update_failing_payment_method( $subscription, $renewal_order ) {
310
		update_post_meta( ( $this->wc_pre_30 ? $subscription->id : $subscription->get_id() ), '_stripe_customer_id', $renewal_order->stripe_customer_id );
311
		update_post_meta( ( $this->wc_pre_30 ? $subscription->id : $subscription->get_id() ), '_stripe_card_id', $renewal_order->stripe_card_id );
312
	}
313
314
	/**
315
	 * Include the payment meta data required to process automatic recurring payments so that store managers can
316
	 * manually set up automatic recurring payments for a customer via the Edit Subscriptions screen in 2.0+.
317
	 *
318
	 * @since 2.5
319
	 * @param array $payment_meta associative array of meta data required for automatic payments
320
	 * @param WC_Subscription $subscription An instance of a subscription object
321
	 * @return array
322
	 */
323
	public function add_subscription_payment_meta( $payment_meta, $subscription ) {
324
		$payment_meta[ $this->id ] = array(
325
			'post_meta' => array(
326
				'_stripe_customer_id' => array(
327
					'value' => get_post_meta( ( $this->wc_pre_30 ? $subscription->id : $subscription->get_id() ), '_stripe_customer_id', true ),
328
					'label' => 'Stripe Customer ID',
329
				),
330
				'_stripe_card_id' => array(
331
					'value' => get_post_meta( ( $this->wc_pre_30 ? $subscription->id : $subscription->get_id() ), '_stripe_card_id', true ),
332
					'label' => 'Stripe Card ID',
333
				),
334
			),
335
		);
336
		return $payment_meta;
337
	}
338
339
	/**
340
	 * Validate the payment meta data required to process automatic recurring payments so that store managers can
341
	 * manually set up automatic recurring payments for a customer via the Edit Subscriptions screen in 2.0+.
342
	 *
343
	 * @since 2.5
344
	 * @param string $payment_method_id The ID of the payment method to validate
345
	 * @param array $payment_meta associative array of meta data required for automatic payments
346
	 * @return array
347
	 */
348
	public function validate_subscription_payment_meta( $payment_method_id, $payment_meta ) {
349
		if ( $this->id === $payment_method_id ) {
350
351
			if ( ! isset( $payment_meta['post_meta']['_stripe_customer_id']['value'] ) || empty( $payment_meta['post_meta']['_stripe_customer_id']['value'] ) ) {
352
				throw new Exception( 'A "_stripe_customer_id" value is required.' );
353
			} elseif ( 0 !== strpos( $payment_meta['post_meta']['_stripe_customer_id']['value'], 'cus_' ) ) {
354
				throw new Exception( 'Invalid customer ID. A valid "_stripe_customer_id" must begin with "cus_".' );
355
			}
356
357
			if ( ! empty( $payment_meta['post_meta']['_stripe_card_id']['value'] ) && 0 !== strpos( $payment_meta['post_meta']['_stripe_card_id']['value'], 'card_' ) ) {
358
				throw new Exception( 'Invalid card ID. A valid "_stripe_card_id" must begin with "card_".' );
359
			}
360
		}
361
	}
362
363
	/**
364
	 * Render the payment method used for a subscription in the "My Subscriptions" table
365
	 *
366
	 * @since 1.7.5
367
	 * @param string $payment_method_to_display the default payment method text to display
368
	 * @param WC_Subscription $subscription the subscription details
369
	 * @return string the subscription payment method
370
	 */
371
	public function maybe_render_subscription_payment_method( $payment_method_to_display, $subscription ) {
372
		$customer_user = $this->wc_pre_30 ? $subscription->customer_user : $subscription->get_customer_id();
373
374
		// bail for other payment methods
375
		if ( $this->id !== ( $this->wc_pre_30 ? $subscription->payment_method : $subscription->get_payment_method() ) || ! $customer_user ) {
376
			return $payment_method_to_display;
377
		}
378
379
		$stripe_customer    = new WC_Stripe_Customer();
380
		$stripe_customer_id = get_post_meta( ( $this->wc_pre_30 ? $subscription->id : $subscription->get_id() ), '_stripe_customer_id', true );
381
		$stripe_card_id     = get_post_meta( ( $this->wc_pre_30 ? $subscription->id : $subscription->get_id() ), '_stripe_card_id', true );
382
383
		// If we couldn't find a Stripe customer linked to the subscription, fallback to the user meta data.
384
		if ( ! $stripe_customer_id || ! is_string( $stripe_customer_id ) ) {
385
			$user_id            = $customer_user;
386
			$stripe_customer_id = get_user_meta( $user_id, '_stripe_customer_id', true );
387
			$stripe_card_id     = get_user_meta( $user_id, '_stripe_card_id', true );
388
		}
389
390
		// If we couldn't find a Stripe customer linked to the account, fallback to the order meta data.
391
		if ( ( ! $stripe_customer_id || ! is_string( $stripe_customer_id ) ) && false !== $subscription->order ) {
392
			$stripe_customer_id = get_post_meta( ( $this->wc_pre_30 ? $subscription->order->id : $subscription->get_parent_id() ), '_stripe_customer_id', true );
393
			$stripe_card_id     = get_post_meta( ( $this->wc_pre_30 ? $subscription->order->id : $subscription->get_parent_id() ), '_stripe_card_id', true );
394
		}
395
396
		$stripe_customer->set_id( $stripe_customer_id );
397
		$cards = $stripe_customer->get_cards();
398
399
		if ( $cards ) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $cards 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...
400
			$found_card = false;
401
			foreach ( $cards as $card ) {
402
				if ( $card->id === $stripe_card_id ) {
403
					$found_card                = true;
404
					$payment_method_to_display = sprintf( __( 'Via %1$s card ending in %2$s', 'woocommerce-gateway-stripe' ), ( isset( $card->type ) ? $card->type : $card->brand ), $card->last4 );
405
					break;
406
				}
407
			}
408
			if ( ! $found_card ) {
409
				$payment_method_to_display = sprintf( __( 'Via %1$s card ending in %2$s', 'woocommerce-gateway-stripe' ), ( isset( $cards[0]->type ) ? $cards[0]->type : $cards[0]->brand ), $cards[0]->last4 );
410
			}
411
		}
412
413
		return $payment_method_to_display;
414
	}
415
416
	/**
417
	 * Logs
418
	 *
419
	 * @since 3.1.0
420
	 * @version 3.1.0
421
	 *
422
	 * @param string $message
423
	 */
424
	public function log( $message ) {
425
		$options = get_option( 'woocommerce_stripe_settings' );
426
427
		if ( 'yes' === $options['logging'] ) {
428
			WC_Stripe::log( $message );
429
		}
430
	}
431
}
432