Test Failed
Push — feature/mollie-connect ( e02179 )
by Reüel
07:33
created

src/Gateway.php (1 issue)

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
	public $client;
40
41
	/**
42
	 * Connect.
43
	 *
44
	 * @var Connect
45
	 */
46
	public $connect;
47
48
	/**
49
	 * Meta key for customer ID.
50
	 *
51
	 * @var string
52
	 */
53
	private $meta_key_customer_id = '_pronamic_pay_mollie_customer_id';
54
55
	/**
56
	 * Constructs and initializes an Mollie gateway
57
	 *
58
	 * @param Config $config Config.
59
	 */
60
	public function __construct( Config $config ) {
61
		parent::__construct( $config );
62
63
		$this->set_method( self::METHOD_HTTP_REDIRECT );
64
65
		// Supported features.
66
		$this->supports = array(
67
			'payment_status_request',
68
			'recurring_direct_debit',
69
			'recurring_credit_card',
70
			'recurring',
71
			'webhook',
72
			'webhook_log',
73
			'webhook_no_config',
74
		);
75
76
		// Connect.
77
		$this->connect = new Connect( $config->id );
78
		$this->connect->set_access_token( $config->access_token );
79
		$this->connect->set_access_token_valid_until( $config->access_token_valid_until );
80
		$this->connect->set_refresh_token( $config->refresh_token );
81
82
		// Client.
83
		$this->client = new Client( $this->connect );
84
		$this->client->set_api_key( $config->api_key );
85
		$this->client->set_mode( $config->mode );
86
87
		// Mollie customer ID meta key.
88
		if ( self::MODE_TEST === $config->mode ) {
89
			$this->meta_key_customer_id = '_pronamic_pay_mollie_customer_id_test';
90
		}
91
92
		// Actions.
93
		add_action( 'pronamic_payment_status_update', array( $this, 'copy_customer_id_to_wp_user' ), 99, 1 );
94
	}
95
96
	/**
97
	 * Get issuers
98
	 *
99
	 * @see Core_Gateway::get_issuers()
100
	 * @return array<int, array<string, array<string>>>
101
	 */
102
	public function get_issuers() {
103
		$groups = array();
104
105
		try {
106
			$result = $this->client->get_issuers();
107
108
			$groups[] = array(
109
				'options' => $result,
110
			);
111
		} catch ( Error $e ) {
112
			// Catch Mollie error.
113
			$error = new \WP_Error(
114
				'mollie_error',
115
				sprintf( '%1$s (%2$s) - %3$s', $e->get_title(), $e->getCode(), $e->get_detail() )
116
			);
117
118
			$this->set_error( $error );
119
		} catch ( \Exception $e ) {
120
			// Catch exceptions.
121
			$error = new \WP_Error( 'mollie_error', $e->getMessage() );
122
123
			$this->set_error( $error );
124
		}
125
126
		return $groups;
127
	}
128
129
	/**
130
	 * Get available payment methods.
131
	 *
132
	 * @see Core_Gateway::get_available_payment_methods()
133
	 * @return array<string>
134
	 */
135
	public function get_available_payment_methods() {
136
		$payment_methods = array();
137
138
		// Set sequence types to get payment methods for.
139
		$sequence_types = array( Sequence::ONE_OFF, Sequence::RECURRING, Sequence::FIRST );
140
141
		$results = array();
142
143
		foreach ( $sequence_types as $sequence_type ) {
144
			// Get active payment methods for Mollie account.
145
			try {
146
				$result = $this->client->get_payment_methods( $sequence_type );
147
			} catch ( Error $e ) {
148
				// Catch Mollie error.
149
				$error = new \WP_Error(
150
					'mollie_error',
151
					sprintf( '%1$s (%2$s) - %3$s', $e->get_title(), $e->getCode(), $e->get_detail() )
152
				);
153
154
				$this->set_error( $error );
155
156
				break;
157
			} catch ( \Exception $e ) {
158
				// Catch exceptions.
159
				$error = new \WP_Error( 'mollie_error', $e->getMessage() );
160
161
				$this->set_error( $error );
162
163
				break;
164
			}
165
166
			if ( Sequence::FIRST === $sequence_type ) {
167
				foreach ( $result as $method => $title ) {
168
					unset( $result[ $method ] );
169
170
					// Get WordPress payment method for direct debit method.
171
					$method         = Methods::transform_gateway_method( $method );
172
					$payment_method = array_search( $method, PaymentMethods::get_recurring_methods(), true );
173
174
					if ( $payment_method ) {
175
						$results[ $payment_method ] = $title;
176
					}
177
				}
178
			}
179
180
			if ( is_array( $result ) ) {
181
				$results = array_merge( $results, $result );
182
			}
183
		}
184
185
		// Transform to WordPress payment methods.
186
		foreach ( $results as $method => $title ) {
187
			if ( PaymentMethods::is_recurring_method( $method ) ) {
188
				$payment_method = $method;
189
			} else {
190
				$payment_method = Methods::transform_gateway_method( $method );
191
			}
192
193
			if ( $payment_method ) {
194
				$payment_methods[] = $payment_method;
195
			}
196
		}
197
198
		$payment_methods = array_unique( $payment_methods );
199
200
		return $payment_methods;
201
	}
202
203
	/**
204
	 * Get supported payment methods
205
	 *
206
	 * @see Pronamic_WP_Pay_Gateway::get_supported_payment_methods()
207
	 * @return array<string>
208
	 */
209
	public function get_supported_payment_methods() {
210
		return array(
211
			PaymentMethods::BANCONTACT,
212
			PaymentMethods::BANK_TRANSFER,
213
			PaymentMethods::BELFIUS,
214
			PaymentMethods::CREDIT_CARD,
215
			PaymentMethods::DIRECT_DEBIT,
216
			PaymentMethods::DIRECT_DEBIT_BANCONTACT,
217
			PaymentMethods::DIRECT_DEBIT_IDEAL,
218
			PaymentMethods::DIRECT_DEBIT_SOFORT,
219
			PaymentMethods::EPS,
220
			PaymentMethods::GIROPAY,
221
			PaymentMethods::IDEAL,
222
			PaymentMethods::KBC,
223
			PaymentMethods::PAYPAL,
224
			PaymentMethods::SOFORT,
225
		);
226
	}
227
228
	/**
229
	 * Get webhook URL for Mollie.
230
	 *
231
	 * @return string|null
232
	 */
233
	public function get_webhook_url() {
234
		$url = home_url( '/' );
235
236
		$host = wp_parse_url( $url, PHP_URL_HOST );
237
238
		if ( is_array( $host ) ) {
239
			// Parsing failure.
240
			$host = '';
241
		}
242
243
		if ( 'localhost' === $host ) {
244
			// Mollie doesn't allow localhost.
245
			return null;
246
		} elseif ( '.dev' === substr( $host, -4 ) ) {
247
			// Mollie doesn't allow the .dev TLD.
248
			return null;
249
		} elseif ( '.local' === substr( $host, -6 ) ) {
250
			// Mollie doesn't allow the .local TLD.
251
			return null;
252
		} elseif ( '.test' === substr( $host, -5 ) ) {
253
			// Mollie doesn't allow the .test TLD.
254
			return null;
255
		}
256
257
		$url = add_query_arg( 'mollie_webhook', '', $url );
258
259
		return $url;
260
	}
261
262
	/**
263
	 * Start
264
	 *
265
	 * @see Pronamic_WP_Pay_Gateway::start()
266
	 * @param Payment $payment Payment.
267
	 * @return void
268
	 */
269
	public function start( Payment $payment ) {
270
		$request = new PaymentRequest(
271
			AmountTransformer::transform( $payment->get_total_amount() ),
272
			\strval( $payment->get_description() )
273
		);
274
275
		$request->redirect_url = $payment->get_return_url();
276
		$request->webhook_url  = $this->get_webhook_url();
277
278
		// Locale.
279
		$customer = $payment->get_customer();
280
281
		if ( null !== $customer ) {
282
			$request->locale = LocaleHelper::transform( $customer->get_locale() );
283
		}
284
285
		// Customer ID.
286
		$customer_id = $this->get_customer_id_for_payment( $payment );
287
288
		if ( is_string( $customer_id ) && ! empty( $customer_id ) ) {
289
			$request->customer_id = $customer_id;
290
		}
291
292
		// Payment method.
293
		$payment_method = $payment->get_method();
294
295
		// Recurring payment method.
296
		$is_recurring_method = ( $payment->get_subscription() && PaymentMethods::is_recurring_method( $payment_method ) );
297
298
		if ( false === $is_recurring_method ) {
299
			// Always use 'direct debit mandate via iDEAL/Bancontact/Sofort' payment methods as recurring method.
300
			$is_recurring_method = PaymentMethods::is_direct_debit_method( $payment_method );
301
		}
302
303
		if ( $is_recurring_method ) {
304
			$request->sequence_type = $payment->get_recurring() ? Sequence::RECURRING : Sequence::FIRST;
305
306
			if ( Sequence::FIRST === $request->sequence_type ) {
307
				$payment_method = PaymentMethods::get_first_payment_method( $payment_method );
308
			}
309
310
			if ( Sequence::RECURRING === $request->sequence_type ) {
311
				$payment->set_action_url( $payment->get_return_url() );
312
			}
313
		}
314
315
		// Leap of faith if the WordPress payment method could not transform to a Mollie method?
316
		$request->method = Methods::transform( $payment_method, $payment_method );
317
318
		// Issuer.
319
		if ( Methods::IDEAL === $request->method ) {
320
			$request->issuer = $payment->get_issuer();
321
		}
322
323
		// Due date.
324
		try {
325
			$due_date = new DateTime( sprintf( '+%s days', $this->config->due_date_days ) );
326
		} catch ( \Exception $e ) {
327
			$due_date = null;
328
		}
329
330
		$request->set_due_date( $due_date );
331
332
		// Create payment.
333
		$result = $this->client->create_payment( $request );
334
335
		// Set transaction ID.
336
		if ( isset( $result->id ) ) {
337
			$payment->set_transaction_id( $result->id );
338
		}
339
340
		// Set expiry date.
341
		/* phpcs:disable WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase */
342
		if ( isset( $result->expiresAt ) ) {
343
			try {
344
				$expires_at = new DateTime( $result->expiresAt );
345
			} catch ( \Exception $e ) {
346
				$expires_at = null;
347
			}
348
349
			$payment->set_expiry_date( $expires_at );
350
		}
351
		/* phpcs:enable WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase */
352
353
		// Set status.
354
		if ( isset( $result->status ) ) {
355
			$payment->set_status( Statuses::transform( $result->status ) );
356
		}
357
358
		// Set bank transfer recipient details.
359
		if ( isset( $result->details ) ) {
360
			$bank_transfer_recipient_details = $payment->get_bank_transfer_recipient_details();
361
362
			if ( null === $bank_transfer_recipient_details ) {
363
				$bank_transfer_recipient_details = new BankTransferDetails();
364
365
				$payment->set_bank_transfer_recipient_details( $bank_transfer_recipient_details );
366
			}
367
368
			$bank_details = $bank_transfer_recipient_details->get_bank_account();
369
370
			if ( null === $bank_details ) {
371
				$bank_details = new BankAccountDetails();
372
373
				$bank_transfer_recipient_details->set_bank_account( $bank_details );
374
			}
375
376
			$details = $result->details;
377
378
			/*
379
			 * @codingStandardsIgnoreStart
380
			 *
381
			 * Ignore coding standards because of sniff WordPress.NamingConventions.ValidVariableName.NotSnakeCaseMemberVar
382
			 */
383
			if ( isset( $details->bankName ) ) {
384
				/**
385
				 * Set `bankName` as bank details name, as result "Stichting Mollie Payments"
386
				 * is not the name of a bank, but the account holder name.
387
				 */
388
				$bank_details->set_name( $details->bankName );
389
			}
390
391
			if ( isset( $details->bankAccount ) ) {
392
				$bank_details->set_iban( $details->bankAccount );
393
			}
394
395
			if ( isset( $details->bankBic ) ) {
396
				$bank_details->set_bic( $details->bankBic );
397
			}
398
399
			if ( isset( $details->transferReference ) ) {
400
				$bank_transfer_recipient_details->set_reference( $details->transferReference );
401
			}
402
			// @codingStandardsIgnoreEnd
403
		}
404
405
		// Set action URL.
406
		if ( isset( $result->_links ) ) {
407
			if ( isset( $result->_links->checkout->href ) ) {
408
				$payment->set_action_url( $result->_links->checkout->href );
409
			}
410
		}
411
	}
412
413
	/**
414
	 * Update status of the specified payment
415
	 *
416
	 * @param Payment $payment Payment.
417
	 * @return void
418
	 */
419
	public function update_status( Payment $payment ) {
420
		$transaction_id = $payment->get_transaction_id();
421
422
		if ( null === $transaction_id ) {
423
			return;
424
		}
425
426
		$mollie_payment = $this->client->get_payment( $transaction_id );
427
428
		if ( isset( $mollie_payment->status ) ) {
429
			$payment->set_status( Statuses::transform( $mollie_payment->status ) );
430
		}
431
432
		if ( isset( $mollie_payment->details ) ) {
433
			$consumer_bank_details = $payment->get_consumer_bank_details();
434
435
			if ( null === $consumer_bank_details ) {
436
				$consumer_bank_details = new BankAccountDetails();
437
438
				$payment->set_consumer_bank_details( $consumer_bank_details );
439
			}
440
441
			$details = $mollie_payment->details;
442
443
			/*
444
			 * @codingStandardsIgnoreStart
445
			 *
446
			 * Ignore coding standards because of sniff WordPress.NamingConventions.ValidVariableName.NotSnakeCaseMemberVar
447
			 */
448
			if ( isset( $details->consumerName ) ) {
449
				$consumer_bank_details->set_name( $details->consumerName );
450
			}
451
452
			if ( isset( $details->cardHolder ) ) {
453
				$consumer_bank_details->set_name( $details->cardHolder );
454
			}
455
456
			if ( isset( $details->cardNumber ) ) {
457
				// The last four digits of the card number.
458
				$consumer_bank_details->set_account_number( $details->cardNumber );
459
			}
460
461
			if ( isset( $details->cardCountryCode ) ) {
462
				// The ISO 3166-1 alpha-2 country code of the country the card was issued in.
463
				$consumer_bank_details->set_country( $details->cardCountryCode );
464
			}
465
466
			if ( isset( $details->consumerAccount ) ) {
467
				switch ( $mollie_payment->method ) {
468
					case Methods::BELFIUS:
469
					case Methods::DIRECT_DEBIT:
470
					case Methods::IDEAL:
471
					case Methods::KBC:
472
					case Methods::SOFORT:
473
						$consumer_bank_details->set_iban( $details->consumerAccount );
474
475
						break;
476
					case Methods::BANCONTACT:
477
					case Methods::BANKTRANSFER:
478
					case Methods::PAYPAL:
479
					default:
480
						$consumer_bank_details->set_account_number( $details->consumerAccount );
481
482
						break;
483
				}
484
			}
485
486
			if ( isset( $details->consumerBic ) ) {
487
				$consumer_bank_details->set_bic( $details->consumerBic );
488
			}
489
			// @codingStandardsIgnoreEnd
490
		}
491
	}
492
493
	/**
494
	 * Get Mollie customers for the specified payment.
495
	 *
496
	 * @param Payment $payment Payment.
497
	 * @return array<string>
498
	 */
499
	private function get_customer_ids_for_payment( Payment $payment ) {
500
		$customer = $payment->get_customer();
501
502
		if ( null === $customer ) {
503
			return array();
504
		}
505
506
		$user_id = $customer->get_user_id();
507
508
		if ( empty( $user_id ) ) {
509
			return array();
510
		}
511
512
		return $this->get_customer_ids_for_user( $user_id );
513
	}
514
515
	/**
516
	 * Get Mollie customers for the specified WordPress user ID.
517
	 *
518
	 * @param int $user_id WordPress user ID.
519
	 * @return array<string>
520
	 */
521
	private function get_customer_ids_for_user( $user_id ) {
522
		$customer_query = new CustomerQuery( array(
523
			'user_id' => $user_id,
524
		) );
525
526
		$customers = $customer_query->get_customers();
527
528
		$customer_ids = wp_list_pluck( $customers, 'mollie_id' );
529
530
		return $customer_ids;
531
	}
532
533
	/**
534
	 * Get first existing customer from customers list.
535
	 *
536
	 * @param array $customers Customers.
537
	 * @return string|null
538
	 */
539
	private function get_first_existing_customer_id( $customers ) {
540
		foreach ( $customer_ids as $customer_id ) {
541
			$customer = $this->client->get_customer( $customer_id );
542
543
			if ( null !== $customer ) {
544
				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...
545
			}
546
		}
547
548
		return null;
549
	}
550
551
	/**
552
	 * Get Mollie customer ID for payment.
553
	 *
554
	 * @param Payment $payment Payment.
555
	 * @return bool|string
556
	 */
557
	public function get_customer_id_for_payment( Payment $payment ) {
558
		$customer = $payment->get_customer();
559
560
		// Get WordPress user ID from payment customer.
561
		$user_id = ( null === $customer ? null : $customer->get_user_id() );
562
563
		// Get Mollie customer ID from user meta.
564
		$customer_id = $this->get_customer_id_by_wp_user_id( $user_id );
565
566
		$subscription = $payment->get_subscription();
567
568
		// Get customer ID from subscription meta.
569
		if ( $subscription ) {
570
			$subscription_customer_id = $subscription->get_meta( 'mollie_customer_id' );
571
572
			// Try to get (legacy) customer ID from first payment.
573
			if ( empty( $subscription_customer_id ) && $subscription->get_first_payment() ) {
574
				$first_payment = $subscription->get_first_payment();
575
576
				$subscription_customer_id = $first_payment->get_meta( 'mollie_customer_id' );
577
			}
578
579
			if ( ! empty( $subscription_customer_id ) ) {
580
				$customer_id = $subscription_customer_id;
581
			}
582
		}
583
584
		// Create new customer if the customer does not exist at Mollie.
585
		if ( ( empty( $customer_id ) || null === $this->client->get_customer( $customer_id ) ) && Core_Recurring::RECURRING !== $payment->recurring_type ) {
586
			$customer_name = null;
587
588
			if ( null !== $customer && null !== $customer->get_name() ) {
589
				$customer_name = strval( $customer->get_name() );
590
			}
591
592
			$customer_id = $this->client->create_customer( $payment->get_email(), $customer_name );
593
594
			$this->update_wp_user_customer_id( $user_id, $customer_id );
595
		}
596
597
		// Store customer ID in subscription meta.
598
		if ( $subscription && empty( $subscription_customer_id ) && ! empty( $customer_id ) ) {
599
			$subscription->set_meta( 'mollie_customer_id', $customer_id );
600
		}
601
602
		// Copy customer ID from subscription to user meta.
603
		$this->copy_customer_id_to_wp_user( $payment );
604
605
		return $customer_id;
606
	}
607
608
	/**
609
	 * Get Mollie customer ID by the specified WordPress user ID.
610
	 *
611
	 * @param int $user_id WordPress user ID.
612
	 * @return string|bool
613
	 */
614
	public function get_customer_id_by_wp_user_id( $user_id ) {
615
		if ( empty( $user_id ) ) {
616
			return false;
617
		}
618
619
		return get_user_meta( $user_id, $this->meta_key_customer_id, true );
620
	}
621
622
	/**
623
	 * Update Mollie customer ID meta for WordPress user.
624
	 *
625
	 * @param int    $user_id     WordPress user ID.
626
	 * @param string $customer_id Mollie Customer ID.
627
	 * @return void
628
	 */
629
	private function update_wp_user_customer_id( $user_id, $customer_id ) {
630
		if ( empty( $user_id ) || is_bool( $user_id ) ) {
631
			return;
632
		}
633
634
		if ( ! is_string( $customer_id ) || empty( $customer_id ) || 1 === strlen( $customer_id ) ) {
635
			return;
636
		}
637
638
		update_user_meta( $user_id, $this->meta_key_customer_id, $customer_id );
639
	}
640
641
	/**
642
	 * Copy Mollie customer ID from subscription meta to WordPress user meta.
643
	 *
644
	 * @param Payment $payment Payment.
645
	 * @return void
646
	 */
647
	public function copy_customer_id_to_wp_user( Payment $payment ) {
648
		if ( $this->config->id !== $payment->config_id ) {
649
			return;
650
		}
651
652
		$subscription = $payment->get_subscription();
653
654
		if ( ! $subscription ) {
655
			return;
656
		}
657
658
		$customer = $subscription->get_customer();
659
660
		if ( null === $customer ) {
661
			return;
662
		}
663
664
		$user_id = $customer->get_user_id();
665
666
		if ( empty( $user_id ) ) {
667
			return;
668
		}
669
670
		// Get customer ID from subscription meta.
671
		$customer_id = $subscription->get_meta( 'mollie_customer_id' );
672
673
		$user_customer_id = $this->get_customer_id_by_wp_user_id( $user_id );
674
675
		if ( empty( $user_customer_id ) ) {
676
			// Set customer ID as user meta.
677
			$this->update_wp_user_customer_id( $user_id, (string) $customer_id );
678
		}
679
	}
680
}
681