Completed
Pull Request — master (#237)
by Roy
04:06
created

process_subscription_payment()   C

Complexity

Conditions 7
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.7272
c 0
b 0
f 0
cc 7
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
	/**
14
	 * Constructor
15
	 */
16
	public function __construct() {
17
		parent::__construct();
18
19
		if ( class_exists( 'WC_Subscriptions_Order' ) ) {
20
			add_action( 'woocommerce_scheduled_subscription_payment_' . $this->id, array( $this, 'scheduled_subscription_payment' ), 10, 2 );
21
			add_action( 'wcs_resubscribe_order_created', array( $this, 'delete_resubscribe_meta' ), 10 );
22
			add_action( 'wcs_renewal_order_created', array( $this, 'delete_renewal_meta' ), 10 );
23
			add_action( 'woocommerce_subscription_failing_payment_method_updated_stripe', array( $this, 'update_failing_payment_method' ), 10, 2 );
24
25
			// display the credit card used for a subscription in the "My Subscriptions" table
26
			add_filter( 'woocommerce_my_subscriptions_payment_method', array( $this, 'maybe_render_subscription_payment_method' ), 10, 2 );
27
28
			// allow store managers to manually set Stripe as the payment method on a subscription
29
			add_filter( 'woocommerce_subscription_payment_meta', array( $this, 'add_subscription_payment_meta' ), 10, 2 );
30
			add_filter( 'woocommerce_subscription_validate_payment_meta', array( $this, 'validate_subscription_payment_meta' ), 10, 2 );
31
		}
32
33
		if ( class_exists( 'WC_Pre_Orders_Order' ) ) {
34
			add_action( 'wc_pre_orders_process_pre_order_completion_payment_' . $this->id, array( $this, 'process_pre_order_release_payment' ) );
35
		}
36
	}
37
38
	/**
39
	 * Is $order_id a subscription?
40
	 * @param  int  $order_id
41
	 * @return boolean
42
	 */
43
	protected function is_subscription( $order_id ) {
44
		return ( function_exists( 'wcs_order_contains_subscription' ) && ( wcs_order_contains_subscription( $order_id ) || wcs_is_subscription( $order_id ) || wcs_order_contains_renewal( $order_id ) ) );
45
	}
46
47
	/**
48
	 * Is $order_id a pre-order?
49
	 * @param  int  $order_id
50
	 * @return boolean
51
	 */
52
	protected function is_pre_order( $order_id ) {
53
		return ( class_exists( 'WC_Pre_Orders_Order' ) && WC_Pre_Orders_Order::order_contains_pre_order( $order_id ) );
54
	}
55
56
	/**
57
	 * Process the payment based on type.
58
	 * @param  int $order_id
59
	 * @return array
60
	 */
61
	public function process_payment( $order_id, $retry = true, $force_customer = false ) {
62
		if ( $this->is_subscription( $order_id ) ) {
63
			// Regular payment with force customer enabled
64
			return parent::process_payment( $order_id, true, true );
65
66
		} elseif ( $this->is_pre_order( $order_id ) ) {
67
			return $this->process_pre_order( $order_id, $retry, $force_customer );
68
69
		} else {
70
			return parent::process_payment( $order_id, $retry, $force_customer );
71
		}
72
	}
73
74
	/**
75
	 * Updates other subscription sources.
76
	 */
77
	protected function save_source( $order, $source ) {
78
		parent::save_source( $order, $source );
79
80
		$wc_pre_30 = version_compare( WC_VERSION, '3.0.0', '<' );
81
		$order_id  = $wc_pre_30 ? $order->id : $order->get_id();
82
83
		// Also store it on the subscriptions being purchased or paid for in the order
84
		if ( function_exists( 'wcs_order_contains_subscription' ) && wcs_order_contains_subscription( $order_id ) ) {
85
			$subscriptions = wcs_get_subscriptions_for_order( $order_id );
86
		} elseif ( function_exists( 'wcs_order_contains_renewal' ) && wcs_order_contains_renewal( $order_id ) ) {
87
			$subscriptions = wcs_get_subscriptions_for_renewal_order( $order_id );
88
		} else {
89
			$subscriptions = array();
90
		}
91
92
		foreach ( $subscriptions as $subscription ) {
93
			$subscription_id = $wc_pre_30 ? $subscription->id : $subscription->get_id();
94
			update_post_meta( $subscription_id, '_stripe_customer_id', $source->customer );
95
			update_post_meta( $subscription_id, '_stripe_card_id', $source->source );
96
		}
97
	}
98
99
	/**
100
	 * process_subscription_payment function.
101
	 * @param mixed $order
102
	 * @param int $amount (default: 0)
103
	 * @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...
104
	 * @param  bool initial_payment
105
	 */
106
	public function process_subscription_payment( $order = '', $amount = 0 ) {
107 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...
108
			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 ) ) );
109
		}
110
111
		// Get source from order
112
		$source = $this->get_order_source( $order );
113
114
		// If no order source was defined, use user source instead.
115
		if ( ! $source->customer ) {
116
			$source = $this->get_source( $order->customer_user );
117
		}
118
119
		// 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...
120
		if ( ! $source->customer ) {
121
			return new WP_Error( 'stripe_error', __( 'Customer not found', 'woocommerce-gateway-stripe' ) );
122
		}
123
124
		$order_id = version_compare( WC_VERSION, '3.0.0', '<' ) ? $order->id : $order->get_id();
125
		$this->log( "Info: Begin processing subscription payment for order {$order_id} for the amount of {$amount}" );
126
127
		// Make the request
128
		$request             = $this->generate_payment_request( $order, $source );
129
		$request['capture']  = 'true';
130
		$request['amount']   = $this->get_stripe_amount( $amount, $request['currency'] );
131
		$request['metadata'] = array(
132
			'payment_type'   => 'recurring',
133
			'site_url'       => esc_url( get_site_url() ),
134
		);
135
		$response            = WC_Stripe_API::request( $request );
136
137
		// Process valid response
138
		if ( is_wp_error( $response ) ) {
139
			if ( 'missing' === $response->get_error_code() ) {
140
				// If we can't link customer to a card, we try to charge by customer ID.
141
				$request             = $this->generate_payment_request( $order, $this->get_source( $order->customer_user ) );
142
				$request['capture']  = 'true';
143
				$request['amount']   = $this->get_stripe_amount( $amount, $request['currency'] );
144
				$request['metadata'] = array(
145
					'payment_type'   => 'recurring',
146
					'site_url'       => esc_url( get_site_url() ),
147
				);
148
				$response          = WC_Stripe_API::request( $request );
149
			}
150
		}
151
152
		$this->process_response( $response, $order );
153
154
		return $response;
155
	}
156
157
	/**
158
	 * Process the pre-order
159
	 * @param int $order_id
160
	 * @return array
161
	 */
162
	public function process_pre_order( $order_id, $retry, $force_customer ) {
163
		if ( WC_Pre_Orders_Order::order_requires_payment_tokenization( $order_id ) ) {
164
			try {
165
				$order = wc_get_order( $order_id );
166
167 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...
168
					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 ) ) );
169
				}
170
171
				$source = $this->get_source( get_current_user_id(), true );
172
173
				// We need a source on file to continue.
174
				if ( empty( $source->customer ) || empty( $source->source ) ) {
175
					throw new Exception( __( 'Unable to store payment details. Please try again.', 'woocommerce-gateway-stripe' ) );
176
				}
177
178
				// Store source to order meta
179
				$this->save_source( $order, $source );
180
181
				// Remove cart
182
				WC()->cart->empty_cart();
183
184
				// Is pre ordered!
185
				WC_Pre_Orders_Order::mark_order_as_pre_ordered( $order );
186
187
				// Return thank you page redirect
188
				return array(
189
					'result'   => 'success',
190
					'redirect' => $this->get_return_url( $order ),
191
				);
192
			} catch ( Exception $e ) {
193
				wc_add_notice( $e->getMessage(), 'error' );
194
				return;
195
			}
196
		} else {
197
			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...
198
		}
199
	}
200
201
	/**
202
	 * Process a pre-order payment when the pre-order is released
203
	 * @param WC_Order $order
204
	 * @return void
205
	 */
206
	public function process_pre_order_release_payment( $order ) {
207
		try {
208
			// Define some callbacks if the first attempt fails.
209
			$retry_callbacks = array(
210
				'remove_order_source_before_retry',
211
				'remove_order_customer_before_retry',
212
			);
213
214
			while ( 1 ) {
215
				$source   = $this->get_order_source( $order );
216
				$response = WC_Stripe_API::request( $this->generate_payment_request( $order, $source ) );
217
218
				if ( is_wp_error( $response ) ) {
219
					if ( 0 === sizeof( $retry_callbacks ) ) {
220
						throw new Exception( $response->get_error_message() );
221
					} else {
222
						$retry_callback = array_shift( $retry_callbacks );
223
						call_user_func( array( $this, $retry_callback ), $order );
224
					}
225
				} else {
226
					// Successful
227
					$this->process_response( $response, $order );
228
					break;
229
				}
230
			}
231
		} catch ( Exception $e ) {
232
			$order_note = sprintf( __( 'Stripe Transaction Failed (%s)', 'woocommerce-gateway-stripe' ), $e->getMessage() );
233
234
			// Mark order as failed if not already set,
235
			// otherwise, make sure we add the order note so we can detect when someone fails to check out multiple times
236
			if ( ! $order->has_status( 'failed' ) ) {
237
				$order->update_status( 'failed', $order_note );
238
			} else {
239
				$order->add_order_note( $order_note );
240
			}
241
		}
242
	}
243
244
	/**
245
	 * Don't transfer Stripe customer/token meta to resubscribe orders.
246
	 * @param int $resubscribe_order The order created for the customer to resubscribe to the old expired/cancelled subscription
247
	 */
248
	public function delete_resubscribe_meta( $resubscribe_order ) {
249
		delete_post_meta( $resubscribe_order->id, '_stripe_customer_id' );
250
		delete_post_meta( $resubscribe_order->id, '_stripe_card_id' );
251
		$this->delete_renewal_meta( $resubscribe_order );
252
	}
253
254
	/**
255
	 * Don't transfer Stripe fee/ID meta to renewal orders.
256
	 * @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...
257
	 */
258
	public function delete_renewal_meta( $renewal_order ) {
259
		delete_post_meta( $renewal_order->id, 'Stripe Fee' );
260
		delete_post_meta( $renewal_order->id, 'Net Revenue From Stripe' );
261
		delete_post_meta( $renewal_order->id, 'Stripe Payment ID' );
262
		return $renewal_order;
263
	}
264
265
	/**
266
	 * scheduled_subscription_payment function.
267
	 *
268
	 * @param $amount_to_charge float The amount to charge.
269
	 * @param $renewal_order WC_Order A WC_Order object created to record the renewal payment.
270
	 */
271
	public function scheduled_subscription_payment( $amount_to_charge, $renewal_order ) {
272
		$response = $this->process_subscription_payment( $renewal_order, $amount_to_charge );
273
274
		if ( is_wp_error( $response ) ) {
275
			$renewal_order->update_status( 'failed', sprintf( __( 'Stripe Transaction Failed (%s)', 'woocommerce-gateway-stripe' ), $response->get_error_message() ) );
276
		}
277
	}
278
279
	/**
280
	 * Remove order meta
281
	 * @param  object $order
282
	 */
283
	public function remove_order_source_before_retry( $order ) {
284
		$order_id = version_compare( WC_VERSION, '3.0.0', '<' ) ? $order->id : $order->get_id();
285
		delete_post_meta( $order_id, '_stripe_card_id' );
286
	}
287
288
	/**
289
	 * Remove order meta
290
	 * @param  object $order
291
	 */
292
	public function remove_order_customer_before_retry( $order ) {
293
		$order_id = version_compare( WC_VERSION, '3.0.0', '<' ) ? $order->id : $order->get_id();
294
		delete_post_meta( $order_id, '_stripe_customer_id' );
295
	}
296
297
	/**
298
	 * Update the customer_id for a subscription after using Stripe to complete a payment to make up for
299
	 * an automatic renewal payment which previously failed.
300
	 *
301
	 * @access public
302
	 * @param WC_Subscription $subscription The subscription for which the failing payment method relates.
303
	 * @param WC_Order $renewal_order The order which recorded the successful payment (to make up for the failed automatic payment).
304
	 * @return void
305
	 */
306
	public function update_failing_payment_method( $subscription, $renewal_order ) {
307
		update_post_meta( $subscription->id, '_stripe_customer_id', $renewal_order->stripe_customer_id );
308
		update_post_meta( $subscription->id, '_stripe_card_id', $renewal_order->stripe_card_id );
309
	}
310
311
	/**
312
	 * Include the payment meta data required to process automatic recurring payments so that store managers can
313
	 * manually set up automatic recurring payments for a customer via the Edit Subscriptions screen in 2.0+.
314
	 *
315
	 * @since 2.5
316
	 * @param array $payment_meta associative array of meta data required for automatic payments
317
	 * @param WC_Subscription $subscription An instance of a subscription object
318
	 * @return array
319
	 */
320
	public function add_subscription_payment_meta( $payment_meta, $subscription ) {
321
		$payment_meta[ $this->id ] = array(
322
			'post_meta' => array(
323
				'_stripe_customer_id' => array(
324
					'value' => get_post_meta( $subscription->id, '_stripe_customer_id', true ),
325
					'label' => 'Stripe Customer ID',
326
				),
327
				'_stripe_card_id' => array(
328
					'value' => get_post_meta( $subscription->id, '_stripe_card_id', true ),
329
					'label' => 'Stripe Card ID',
330
				),
331
			),
332
		);
333
		return $payment_meta;
334
	}
335
336
	/**
337
	 * Validate the payment meta data required to process automatic recurring payments so that store managers can
338
	 * manually set up automatic recurring payments for a customer via the Edit Subscriptions screen in 2.0+.
339
	 *
340
	 * @since 2.5
341
	 * @param string $payment_method_id The ID of the payment method to validate
342
	 * @param array $payment_meta associative array of meta data required for automatic payments
343
	 * @return array
344
	 */
345
	public function validate_subscription_payment_meta( $payment_method_id, $payment_meta ) {
346
		if ( $this->id === $payment_method_id ) {
347
348
			if ( ! isset( $payment_meta['post_meta']['_stripe_customer_id']['value'] ) || empty( $payment_meta['post_meta']['_stripe_customer_id']['value'] ) ) {
349
				throw new Exception( 'A "_stripe_customer_id" value is required.' );
350
			} elseif ( 0 !== strpos( $payment_meta['post_meta']['_stripe_customer_id']['value'], 'cus_' ) ) {
351
				throw new Exception( 'Invalid customer ID. A valid "_stripe_customer_id" must begin with "cus_".' );
352
			}
353
354
			if ( ! empty( $payment_meta['post_meta']['_stripe_card_id']['value'] ) && 0 !== strpos( $payment_meta['post_meta']['_stripe_card_id']['value'], 'card_' ) ) {
355
				throw new Exception( 'Invalid card ID. A valid "_stripe_card_id" must begin with "card_".' );
356
			}
357
		}
358
	}
359
360
	/**
361
	 * Render the payment method used for a subscription in the "My Subscriptions" table
362
	 *
363
	 * @since 1.7.5
364
	 * @param string $payment_method_to_display the default payment method text to display
365
	 * @param WC_Subscription $subscription the subscription details
366
	 * @return string the subscription payment method
367
	 */
368
	public function maybe_render_subscription_payment_method( $payment_method_to_display, $subscription ) {
369
		$wc_pre_30 = version_compare( WC_VERSION, '3.0', '<' );
370
371
		$customer_user = $wc_pre_30 ? $subscription->customer_user : $subscription->get_customer_id();
372
373
		// bail for other payment methods
374
		if ( $this->id !== ( $wc_pre_30 ? $subscription->payment_method : $subscription->get_payment_method() ) || ! $customer_user ) {
375
			return $payment_method_to_display;
376
		}
377
378
		$stripe_customer    = new WC_Stripe_Customer();
379
		$stripe_customer_id = get_post_meta( ( $wc_pre_30 ? $subscription->id : $subscription->get_id() ), '_stripe_customer_id', true );
380
		$stripe_card_id     = get_post_meta( ( $wc_pre_30 ? $subscription->id : $subscription->get_id() ), '_stripe_card_id', true );
381
382
		// If we couldn't find a Stripe customer linked to the subscription, fallback to the user meta data.
383
		if ( ! $stripe_customer_id || ! is_string( $stripe_customer_id ) ) {
384
			$user_id            = $customer_user;
385
			$stripe_customer_id = get_user_meta( $user_id, '_stripe_customer_id', true );
386
			$stripe_card_id     = get_user_meta( $user_id, '_stripe_card_id', true );
387
		}
388
389
		// If we couldn't find a Stripe customer linked to the account, fallback to the order meta data.
390
		if ( ( ! $stripe_customer_id || ! is_string( $stripe_customer_id ) ) && false !== $subscription->order ) {
391
			$stripe_customer_id = get_post_meta( ( $wc_pre_30 ? $subscription->order->id : $subscription->get_parent_id() ), '_stripe_customer_id', true );
392
			$stripe_card_id     = get_post_meta( ( $wc_pre_30 ? $subscription->order->id : $subscription->get_parent_id() ), '_stripe_card_id', true );
393
		}
394
395
		$stripe_customer->set_id( $stripe_customer_id );
396
		$cards = $stripe_customer->get_cards();
397
398
		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...
399
			$found_card = false;
400
			foreach ( $cards as $card ) {
401
				if ( $card->id === $stripe_card_id ) {
402
					$found_card                = true;
403
					$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 );
404
					break;
405
				}
406
			}
407
			if ( ! $found_card ) {
408
				$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 );
409
			}
410
		}
411
412
		return $payment_method_to_display;
413
	}
414
415
	/**
416
	 * Logs
417
	 *
418
	 * @since 3.1.0
419
	 * @version 3.1.0
420
	 *
421
	 * @param string $message
422
	 */
423
	public function log( $message ) {
424
		$options = get_option( 'woocommerce_stripe_settings' );
425
426
		if ( 'yes' === $options['logging'] ) {
427
			WC_Stripe::log( $message );
428
		}
429
	}
430
}
431