Test Failed
Push — develop ( 13fc2a...744f0f )
by Reüel
04:50
created

Gateway::get_first_existing_customer_id()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 10
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 3.3332

Importance

Changes 0
Metric Value
cc 3
eloc 5
c 0
b 0
f 0
nc 3
nop 1
dl 0
loc 10
rs 10
ccs 2
cts 3
cp 0.6667
crap 3.3332
1
<?php
2
/**
3
 * Mollie gateway.
4
 *
5
 * @author    Pronamic <[email protected]>
6
 * @copyright 2005-2020 Pronamic
7
 * @license   GPL-3.0-or-later
8
 * @package   Pronamic\WordPress\Pay
9
 */
10
11
namespace Pronamic\WordPress\Pay\Gateways\Mollie;
12
13
use DateInterval;
14
use Pronamic\WordPress\DateTime\DateTime;
15
use Pronamic\WordPress\Pay\Banks\BankAccountDetails;
16
use Pronamic\WordPress\Pay\Banks\BankTransferDetails;
17
use Pronamic\WordPress\Pay\Core\Gateway as Core_Gateway;
18
use Pronamic\WordPress\Pay\Core\PaymentMethods;
19
use Pronamic\WordPress\Pay\Core\Recurring as Core_Recurring;
20
use Pronamic\WordPress\Pay\Payments\PaymentStatus;
21
use Pronamic\WordPress\Pay\Payments\Payment;
22
23
/**
24
 * Title: Mollie
25
 * Description:
26
 * Copyright: 2005-2020 Pronamic
27
 * Company: Pronamic
28
 *
29
 * @author  Remco Tolsma
30
 * @version 2.0.9
31
 * @since   1.1.0
32
 */
33
class Gateway extends Core_Gateway {
34
	/**
35
	 * Client.
36
	 *
37
	 * @var Client
38
	 */
39
	protected $client;
40
41
	/**
42
	 * Meta key for customer ID.
43
	 *
44
	 * @var string
45
	 */
46
	private $meta_key_customer_id = '_pronamic_pay_mollie_customer_id';
47
48
	/**
49
	 * Constructs and initializes an Mollie gateway
50
	 *
51
	 * @param Config $config Config.
52
	 */
53 39
	public function __construct( Config $config ) {
54 39
		parent::__construct( $config );
55
56 39
		$this->set_method( self::METHOD_HTTP_REDIRECT );
57
58
		// Supported features.
59 39
		$this->supports = array(
60
			'payment_status_request',
61
			'recurring_direct_debit',
62
			'recurring_credit_card',
63
			'recurring',
64
			'webhook',
65
			'webhook_log',
66
			'webhook_no_config',
67
		);
68
69
		// Client.
70 39
		$this->client = new Client( \strval( $config->api_key ) );
71 39
		$this->client->set_mode( $config->mode );
72
73
		// Mollie customer ID meta key.
74 39
		if ( self::MODE_TEST === $config->mode ) {
75 38
			$this->meta_key_customer_id = '_pronamic_pay_mollie_customer_id_test';
76
		}
77
78
		// Actions.
79 39
		add_action( 'pronamic_payment_status_update', array( $this, 'copy_customer_id_to_wp_user' ), 99, 1 );
80 39
	}
81
82
	/**
83
	 * Get issuers
84
	 *
85
	 * @see Core_Gateway::get_issuers()
86
	 * @return array<int, array<string, array<string>>>
87 3
	 */
88 3
	public function get_issuers() {
89
		$groups = array();
90
91 3
		try {
92
			$result = $this->client->get_issuers();
93
94
			$groups[] = array(
95
				'options' => $result,
96 3
			);
97
		} catch ( Error $e ) {
98 3
			// Catch Mollie error.
99 3
			$error = new \WP_Error(
100 3
				'mollie_error',
101
				sprintf( '%1$s (%2$s) - %3$s', $e->get_title(), $e->getCode(), $e->get_detail() )
102
			);
103 3
104
			$this->set_error( $error );
105
		} catch ( \Exception $e ) {
106
			// Catch exceptions.
107
			$error = new \WP_Error( 'mollie_error', $e->getMessage() );
108
109
			$this->set_error( $error );
110
		}
111 3
112
		return $groups;
113
	}
114
115
	/**
116
	 * Get available payment methods.
117
	 *
118
	 * @see Core_Gateway::get_available_payment_methods()
119 2
	 * @return array<string>
120 2
	 */
121
	public function get_available_payment_methods() {
122
		$payment_methods = array();
123 2
124
		// Set sequence types to get payment methods for.
125 2
		$sequence_types = array( Sequence::ONE_OFF, Sequence::RECURRING, Sequence::FIRST );
126
127 2
		$results = array();
128
129
		foreach ( $sequence_types as $sequence_type ) {
130 2
			// Get active payment methods for Mollie account.
131 2
			try {
132
				$result = $this->client->get_payment_methods( $sequence_type );
133
			} catch ( Error $e ) {
134
				// Catch Mollie error.
135
				$error = new \WP_Error(
136
					'mollie_error',
137
					sprintf( '%1$s (%2$s) - %3$s', $e->get_title(), $e->getCode(), $e->get_detail() )
138
				);
139
140
				$this->set_error( $error );
141 2
142
				break;
143 2
			} catch ( \Exception $e ) {
144
				// Catch exceptions.
145 2
				$error = new \WP_Error( 'mollie_error', $e->getMessage() );
146
147 2
				$this->set_error( $error );
148
149
				break;
150 2
			}
151
152
			if ( Sequence::FIRST === $sequence_type ) {
153
				foreach ( $result as $method => $title ) {
154
					unset( $result[ $method ] );
155
156
					// Get WordPress payment method for direct debit method.
157
					$method         = Methods::transform_gateway_method( $method );
158
					$payment_method = array_search( $method, PaymentMethods::get_recurring_methods(), true );
159
160
					if ( $payment_method ) {
161
						$results[ $payment_method ] = $title;
162
					}
163
				}
164 2
			}
165 2
166
			if ( is_array( $result ) ) {
167
				$results = array_merge( $results, $result );
168
			}
169
		}
170 2
171 2
		// Transform to WordPress payment methods.
172
		foreach ( $results as $method => $title ) {
173
			if ( PaymentMethods::is_recurring_method( $method ) ) {
174 2
				$payment_method = $method;
175
			} else {
176
				$payment_method = Methods::transform_gateway_method( $method );
177 2
			}
178 2
179
			if ( $payment_method ) {
180
				$payment_methods[] = $payment_method;
181
			}
182 2
		}
183
184 2
		$payment_methods = array_unique( $payment_methods );
185
186
		return $payment_methods;
187
	}
188
189
	/**
190
	 * Get supported payment methods
191
	 *
192
	 * @see Pronamic_WP_Pay_Gateway::get_supported_payment_methods()
193 2
	 * @return array<string>
194
	 */
195 2
	public function get_supported_payment_methods() {
196
		return array(
197
			PaymentMethods::BANCONTACT,
198
			PaymentMethods::BANK_TRANSFER,
199
			PaymentMethods::BELFIUS,
200
			PaymentMethods::CREDIT_CARD,
201
			PaymentMethods::DIRECT_DEBIT,
202
			PaymentMethods::DIRECT_DEBIT_BANCONTACT,
203
			PaymentMethods::DIRECT_DEBIT_IDEAL,
204
			PaymentMethods::DIRECT_DEBIT_SOFORT,
205
			PaymentMethods::EPS,
206
			PaymentMethods::GIROPAY,
207
			PaymentMethods::IDEAL,
208
			PaymentMethods::KBC,
209
			PaymentMethods::PAYPAL,
210
			PaymentMethods::SOFORT,
211
		);
212
	}
213
214
	/**
215
	 * Get webhook URL for Mollie.
216
	 *
217 4
	 * @return string|null
218 4
	 */
219
	public function get_webhook_url() {
220 4
		$url = home_url( '/' );
221
222 4
		$host = wp_parse_url( $url, PHP_URL_HOST );
223
224
		if ( is_array( $host ) ) {
225
			// Parsing failure.
226
			$host = '';
227 4
		}
228
229 1
		if ( 'localhost' === $host ) {
230 3
			// Mollie doesn't allow localhost.
231
			return null;
232 1
		} elseif ( '.dev' === substr( $host, -4 ) ) {
233 2
			// Mollie doesn't allow the .dev TLD.
234
			return null;
235 1
		} elseif ( '.local' === substr( $host, -6 ) ) {
236 1
			// Mollie doesn't allow the .local TLD.
237
			return null;
238
		} elseif ( '.test' === substr( $host, -5 ) ) {
239
			// Mollie doesn't allow the .test TLD.
240
			return null;
241 1
		}
242
243 1
		$url = add_query_arg( 'mollie_webhook', '', $url );
244
245
		return $url;
246
	}
247
248
	/**
249
	 * Start
250
	 *
251
	 * @see Pronamic_WP_Pay_Gateway::start()
252
	 * @param Payment $payment Payment.
253
	 * @return void
254
	 */
255
	public function start( Payment $payment ) {
256
		$request = new PaymentRequest(
257
			AmountTransformer::transform( $payment->get_total_amount() ),
258
			\strval( $payment->get_description() )
259
		);
260
261
		$request->redirect_url = $payment->get_return_url();
262
		$request->webhook_url  = $this->get_webhook_url();
263
264
		// Locale.
265
		$customer = $payment->get_customer();
266
267
		if ( null !== $customer ) {
268
			$request->locale = LocaleHelper::transform( $customer->get_locale() );
269
		}
270
271
		// Customer ID.
272
		$customer_id = $this->get_customer_id_for_payment( $payment );
273
274
		if ( is_string( $customer_id ) && ! empty( $customer_id ) ) {
275
			$request->customer_id = $customer_id;
276
		}
277
278
		// Payment method.
279
		$payment_method = $payment->get_method();
280
281
		// Recurring payment method.
282
		$is_recurring_method = ( $payment->get_subscription() && PaymentMethods::is_recurring_method( $payment_method ) );
283
284
		if ( false === $is_recurring_method ) {
285
			// Always use 'direct debit mandate via iDEAL/Bancontact/Sofort' payment methods as recurring method.
286
			$is_recurring_method = PaymentMethods::is_direct_debit_method( $payment_method );
287
		}
288
289
		if ( $is_recurring_method ) {
290
			$request->sequence_type = $payment->get_recurring() ? Sequence::RECURRING : Sequence::FIRST;
291
292
			if ( Sequence::FIRST === $request->sequence_type ) {
293
				$payment_method = PaymentMethods::get_first_payment_method( $payment_method );
294
			}
295
296
			if ( Sequence::RECURRING === $request->sequence_type ) {
297
				$payment->set_action_url( $payment->get_return_url() );
298
			}
299
		}
300
301
		// Leap of faith if the WordPress payment method could not transform to a Mollie method?
302
		$request->method = Methods::transform( $payment_method, $payment_method );
303
304
		// Issuer.
305
		if ( Methods::IDEAL === $request->method ) {
306
			$request->issuer = $payment->get_issuer();
307
		}
308
309
		// Due date.
310
		try {
311
			$due_date = new DateTime( sprintf( '+%s days', $this->config->due_date_days ) );
312
		} catch ( \Exception $e ) {
313
			$due_date = null;
314
		}
315
316
		$request->set_due_date( $due_date );
317
318
		// Create payment.
319
		$result = $this->client->create_payment( $request );
320
321
		// Set transaction ID.
322
		if ( isset( $result->id ) ) {
323
			$payment->set_transaction_id( $result->id );
324
		}
325
326
		// Set expiry date.
327
		/* phpcs:disable WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase */
328
		if ( isset( $result->expiresAt ) ) {
329
			try {
330
				$expires_at = new DateTime( $result->expiresAt );
331
			} catch ( \Exception $e ) {
332
				$expires_at = null;
333
			}
334
335
			$payment->set_expiry_date( $expires_at );
336
		}
337
		/* phpcs:enable WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase */
338
339
		// Set status.
340
		if ( isset( $result->status ) ) {
341
			$payment->set_status( Statuses::transform( $result->status ) );
342
		}
343
344
		// Set bank transfer recipient details.
345
		if ( isset( $result->details ) ) {
346
			$bank_transfer_recipient_details = $payment->get_bank_transfer_recipient_details();
347
348
			if ( null === $bank_transfer_recipient_details ) {
349
				$bank_transfer_recipient_details = new BankTransferDetails();
350
351
				$payment->set_bank_transfer_recipient_details( $bank_transfer_recipient_details );
352
			}
353
354
			$bank_details = $bank_transfer_recipient_details->get_bank_account();
355
356
			if ( null === $bank_details ) {
357
				$bank_details = new BankAccountDetails();
358
359
				$bank_transfer_recipient_details->set_bank_account( $bank_details );
360
			}
361
362
			$details = $result->details;
363
364
			/*
365
			 * @codingStandardsIgnoreStart
366
			 *
367
			 * Ignore coding standards because of sniff WordPress.NamingConventions.ValidVariableName.NotSnakeCaseMemberVar
368
			 */
369
			if ( isset( $details->bankName ) ) {
370
				/**
371
				 * Set `bankName` as bank details name, as result "Stichting Mollie Payments"
372
				 * is not the name of a bank, but the account holder name.
373
				 */
374
				$bank_details->set_name( $details->bankName );
375
			}
376
377
			if ( isset( $details->bankAccount ) ) {
378
				$bank_details->set_iban( $details->bankAccount );
379
			}
380
381
			if ( isset( $details->bankBic ) ) {
382
				$bank_details->set_bic( $details->bankBic );
383
			}
384
385
			if ( isset( $details->transferReference ) ) {
386
				$bank_transfer_recipient_details->set_reference( $details->transferReference );
387
			}
388
			// @codingStandardsIgnoreEnd
389
		}
390
391
		// Set action URL.
392
		if ( isset( $result->_links ) ) {
393
			if ( isset( $result->_links->checkout->href ) ) {
394
				$payment->set_action_url( $result->_links->checkout->href );
395
			}
396
		}
397
	}
398
399
	/**
400
	 * Update status of the specified payment
401
	 *
402
	 * @param Payment $payment Payment.
403
	 * @return void
404
	 */
405
	public function update_status( Payment $payment ) {
406
		$transaction_id = $payment->get_transaction_id();
407
408
		if ( null === $transaction_id ) {
409
			return;
410
		}
411
412
		$mollie_payment = $this->client->get_payment( $transaction_id );
413
414
		if ( isset( $mollie_payment->status ) ) {
415
			$payment->set_status( Statuses::transform( $mollie_payment->status ) );
416
		}
417
418
		if ( isset( $mollie_payment->details ) ) {
419
			$consumer_bank_details = $payment->get_consumer_bank_details();
420
421
			if ( null === $consumer_bank_details ) {
422
				$consumer_bank_details = new BankAccountDetails();
423
424
				$payment->set_consumer_bank_details( $consumer_bank_details );
425
			}
426
427
			$details = $mollie_payment->details;
428
429
			/*
430
			 * @codingStandardsIgnoreStart
431
			 *
432
			 * Ignore coding standards because of sniff WordPress.NamingConventions.ValidVariableName.NotSnakeCaseMemberVar
433
			 */
434
			if ( isset( $details->consumerName ) ) {
435
				$consumer_bank_details->set_name( $details->consumerName );
436
			}
437
438
			if ( isset( $details->cardHolder ) ) {
439
				$consumer_bank_details->set_name( $details->cardHolder );
440
			}
441
442
			if ( isset( $details->cardNumber ) ) {
443
				// The last four digits of the card number.
444
				$consumer_bank_details->set_account_number( $details->cardNumber );
445
			}
446
447
			if ( isset( $details->cardCountryCode ) ) {
448
				// The ISO 3166-1 alpha-2 country code of the country the card was issued in.
449
				$consumer_bank_details->set_country( $details->cardCountryCode );
450
			}
451
452
			if ( isset( $details->consumerAccount ) ) {
453
				switch ( $mollie_payment->method ) {
454
					case Methods::BELFIUS:
455
					case Methods::DIRECT_DEBIT:
456
					case Methods::IDEAL:
457
					case Methods::KBC:
458
					case Methods::SOFORT:
459
						$consumer_bank_details->set_iban( $details->consumerAccount );
460
461
						break;
462
					case Methods::BANCONTACT:
463
					case Methods::BANKTRANSFER:
464
					case Methods::PAYPAL:
465
					default:
466
						$consumer_bank_details->set_account_number( $details->consumerAccount );
467
468
						break;
469
				}
470
			}
471
472
			if ( isset( $details->consumerBic ) ) {
473
				$consumer_bank_details->set_bic( $details->consumerBic );
474
			}
475
			// @codingStandardsIgnoreEnd
476
		}
477
	}
478
479
	/**
480
	 * Get Mollie customers for the specified payment.
481
	 *
482
	 * @param Payment $payment Payment.
483 10
	 * @return array<string>
484 10
	 */
485
	private function get_customer_ids_for_payment( Payment $payment ) {
0 ignored issues
show
Unused Code introduced by
The method get_customer_ids_for_payment() is not used, and could be removed.

This check looks for private methods that have been defined, but are not used inside the class.

Loading history...
486
		$customer = $payment->get_customer();
487 10
488
		if ( null === $customer ) {
489
			return array();
490 10
		}
491
492 10
		$user_id = $customer->get_user_id();
493
494
		if ( empty( $user_id ) ) {
495 10
			return array();
496 10
		}
497
498
		return $this->get_customer_ids_for_user( $user_id );
499 10
	}
500 7
501
	/**
502 7
	 * Get Mollie customers for the specified WordPress user ID.
503
	 *
504
	 * @param int $user_id WordPress user ID.
505 10
	 * @return array<string>
506 4
	 */
507
	private function get_customer_ids_for_user( $user_id ) {
508
		$customer_query = new CustomerQuery( array(
509
			'user_id' => $user_id,
510
		) );
511 10
512
		$customers = $customer_query->get_customers();
513
514
		$customer_ids = wp_list_pluck( $customers, 'mollie_id' );
515
516
		return $customer_ids;
517
	}
518
519
	/**
520
	 * Get first existing customer from customers list.
521
	 *
522
	 * @param array $customers Customers.
523
	 * @return string|null
524 10
	 */
525
	private function get_first_existing_customer_id( $customers ) {
0 ignored issues
show
Unused Code introduced by
The parameter $customers is not used and could be removed. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-unused  annotation

525
	private function get_first_existing_customer_id( /** @scrutinizer ignore-unused */ $customers ) {

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
Unused Code introduced by
The method get_first_existing_customer_id() is not used, and could be removed.

This check looks for private methods that have been defined, but are not used inside the class.

Loading history...
526
		foreach ( $customer_ids as $customer_id ) {
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $customer_ids does not exist. Did you maybe mean $customer?
Loading history...
527
			$customer = $this->client->get_customer( $customer_id );
528
529 10
			if ( null !== $customer ) {
530
				return $customer;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $customer returns the type object which is incompatible with the documented return type null|string.
Loading history...
531 10
			}
532
		}
533
534
		return null;
535
	}
536
537
	/**
538
	 * Get Mollie customer ID for payment.
539
	 *
540 27
	 * @param Payment $payment Payment.
541 27
	 * @return bool|string
542 11
	 */
543
	public function get_customer_id_for_payment( Payment $payment ) {
544
		$customer = $payment->get_customer();
545 16
546
		// Get WordPress user ID from payment customer.
547
		$user_id = ( null === $customer ? null : $customer->get_user_id() );
548
549
		// Get Mollie customer ID from user meta.
550
		$customer_id = $this->get_customer_id_by_wp_user_id( $user_id );
551
552
		$subscription = $payment->get_subscription();
553
554
		// Get customer ID from subscription meta.
555 15
		if ( $subscription ) {
556 15
			$subscription_customer_id = $subscription->get_meta( 'mollie_customer_id' );
557 3
558
			// Try to get (legacy) customer ID from first payment.
559
			if ( empty( $subscription_customer_id ) && $subscription->get_first_payment() ) {
560 12
				$first_payment = $subscription->get_first_payment();
561 11
562
				$subscription_customer_id = $first_payment->get_meta( 'mollie_customer_id' );
563
			}
564 4
565 4
			if ( ! empty( $subscription_customer_id ) ) {
566
				$customer_id = $subscription_customer_id;
567
			}
568
		}
569
570
		// Create new customer if the customer does not exist at Mollie.
571
		if ( ( empty( $customer_id ) || null === $this->client->get_customer( $customer_id ) ) && Core_Recurring::RECURRING !== $payment->recurring_type ) {
572
			$customer_name = null;
573 27
574 27
			if ( null !== $customer && null !== $customer->get_name() ) {
575 1
				$customer_name = strval( $customer->get_name() );
576
			}
577
578 26
			$customer_id = $this->client->create_customer( $payment->get_email(), $customer_name );
579
580 26
			$this->update_wp_user_customer_id( $user_id, $customer_id );
581
		}
582
583
		// Store customer ID in subscription meta.
584 26
		if ( $subscription && empty( $subscription_customer_id ) && ! empty( $customer_id ) ) {
585
			$subscription->set_meta( 'mollie_customer_id', $customer_id );
586 26
		}
587 1
588
		// Copy customer ID from subscription to user meta.
589
		$this->copy_customer_id_to_wp_user( $payment );
590 25
591
		return $customer_id;
592 25
	}
593 10
594
	/**
595
	 * Get Mollie customer ID by the specified WordPress user ID.
596
	 *
597 15
	 * @param int $user_id WordPress user ID.
598
	 * @return string|bool
599 15
	 */
600
	public function get_customer_id_by_wp_user_id( $user_id ) {
601 15
		if ( empty( $user_id ) ) {
602
			return false;
603 15
		}
604
605 15
		return get_user_meta( $user_id, $this->meta_key_customer_id, true );
606
	}
607
608
	/**
609
	 * Update Mollie customer ID meta for WordPress user.
610
	 *
611
	 * @param int    $user_id     WordPress user ID.
612
	 * @param string $customer_id Mollie Customer ID.
613
	 * @return void
614
	 */
615
	private function update_wp_user_customer_id( $user_id, $customer_id ) {
616
		if ( empty( $user_id ) || is_bool( $user_id ) ) {
617
			return;
618
		}
619
620
		if ( ! is_string( $customer_id ) || empty( $customer_id ) || 1 === strlen( $customer_id ) ) {
0 ignored issues
show
introduced by
The condition is_string($customer_id) is always true.
Loading history...
621
			return;
622
		}
623
624
		update_user_meta( $user_id, $this->meta_key_customer_id, $customer_id );
625
	}
626
627
	/**
628
	 * Copy Mollie customer ID from subscription meta to WordPress user meta.
629
	 *
630
	 * @param Payment $payment Payment.
631
	 * @return void
632
	 */
633
	public function copy_customer_id_to_wp_user( Payment $payment ) {
634
		if ( $this->config->id !== $payment->config_id ) {
635
			return;
636
		}
637
638
		$subscription = $payment->get_subscription();
639
640
		if ( ! $subscription ) {
641
			return;
642
		}
643
644
		$customer = $subscription->get_customer();
645
646
		if ( null === $customer ) {
647
			return;
648
		}
649
650
		$user_id = $customer->get_user_id();
651
652
		if ( empty( $user_id ) ) {
653
			return;
654
		}
655
656
		// Get customer ID from subscription meta.
657
		$customer_id = $subscription->get_meta( 'mollie_customer_id' );
658
659
		$user_customer_id = $this->get_customer_id_by_wp_user_id( $user_id );
660
661
		if ( empty( $user_customer_id ) ) {
662
			// Set customer ID as user meta.
663
			$this->update_wp_user_customer_id( $user_id, (string) $customer_id );
664
		}
665
	}
666
}
667