Failed Conditions
Push — develop ( e03969...b62619 )
by Remco
04:53
created

Gateway   F

Complexity

Total Complexity 144

Size/Duplication

Total Lines 1079
Duplicated Lines 0 %

Test Coverage

Coverage 29.68%

Importance

Changes 70
Bugs 0 Features 0
Metric Value
eloc 428
c 70
b 0
f 0
dl 0
loc 1079
ccs 119
cts 401
cp 0.2968
rs 2
wmc 144

16 Methods

Rating   Name   Duplication   Size   Complexity  
A get_supported_payment_methods() 0 18 1
B get_available_payment_methods() 0 68 11
A __construct() 0 26 1
A get_webhook_url() 0 25 6
A get_issuers() 0 25 3
A get_customer_ids_for_user() 0 12 1
A get_customer_id_for_payment() 0 6 1
B copy_customer_id_to_wp_user() 0 53 9
B update_subscription_mandate() 0 54 11
F start() 0 243 29
C create_customer_for_payment() 0 60 12
A get_customer_id_for_subscription() 0 17 4
A get_customer_ids_for_payment() 0 28 5
A create_refund() 0 22 3
A get_first_existing_customer_id() 0 23 5
F update_status() 0 230 42

How to fix   Complexity   

Complex Class

Complex classes like Gateway often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use Gateway, and based on these observations, apply Extract Interface, too.

1
<?php
2
/**
3
 * Mollie gateway.
4
 *
5
 * @author    Pronamic <[email protected]>
6
 * @copyright 2005-2021 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 Pronamic\WordPress\DateTime\DateTime;
14
use Pronamic\WordPress\Money\Money;
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\Payments\FailureReason;
20
use Pronamic\WordPress\Pay\Payments\Payment;
0 ignored issues
show
Bug introduced by
This use statement conflicts with another class in this namespace, Pronamic\WordPress\Pay\Gateways\Mollie\Payment. Consider defining an alias.

Let?s assume that you have a directory layout like this:

.
|-- OtherDir
|   |-- Bar.php
|   `-- Foo.php
`-- SomeDir
    `-- Foo.php

and let?s assume the following content of Bar.php:

// Bar.php
namespace OtherDir;

use SomeDir\Foo; // This now conflicts the class OtherDir\Foo

If both files OtherDir/Foo.php and SomeDir/Foo.php are loaded in the same runtime, you will see a PHP error such as the following:

PHP Fatal error:  Cannot use SomeDir\Foo as Foo because the name is already in use in OtherDir/Foo.php

However, as OtherDir/Foo.php does not necessarily have to be loaded and the error is only triggered if it is loaded before OtherDir/Bar.php, this problem might go unnoticed for a while. In order to prevent this error from surfacing, you must import the namespace with a different alias:

// Bar.php
namespace OtherDir;

use SomeDir\Foo as SomeDirFoo; // There is no conflict anymore.
Loading history...
21
use Pronamic\WordPress\Pay\Payments\PaymentStatus;
22
use Pronamic\WordPress\Pay\Subscriptions\Subscription;
23
use Pronamic\WordPress\Pay\Subscriptions\SubscriptionStatus;
24
25
/**
26
 * Title: Mollie
27
 * Description:
28
 * Copyright: 2005-2021 Pronamic
29
 * Company: Pronamic
30
 *
31
 * @author  Remco Tolsma
32
 * @version 2.1.4
33
 * @since   1.1.0
34
 */
35
class Gateway extends Core_Gateway {
36
	/**
37
	 * Client.
38
	 *
39
	 * @var Client
40
	 */
41
	protected $client;
42
43
	/**
44
	 * Config
45
	 *
46
	 * @var Config
47
	 */
48
	protected $config;
49
50
	/**
51
	 * Profile data store.
52
	 *
53
	 * @var ProfileDataStore
54
	 */
55
	private $profile_data_store;
56
57
	/**
58
	 * Customer data store.
59
	 *
60
	 * @var CustomerDataStore
61
	 */
62
	private $customer_data_store;
63
64
	/**
65
	 * Constructs and initializes an Mollie gateway
66
	 *
67
	 * @param Config $config Config.
68
	 */
69 39
	public function __construct( Config $config ) {
70 39
		parent::__construct( $config );
71
72 39
		$this->set_method( self::METHOD_HTTP_REDIRECT );
73
74
		// Supported features.
75 39
		$this->supports = array(
76
			'payment_status_request',
77
			'recurring_direct_debit',
78
			'recurring_credit_card',
79
			'recurring',
80
			'refunds',
81
			'webhook',
82
			'webhook_log',
83
			'webhook_no_config',
84
		);
85
86
		// Client.
87 39
		$this->client = new Client( (string) $config->api_key );
88
89
		// Data Stores.
90 39
		$this->profile_data_store  = new ProfileDataStore();
91 39
		$this->customer_data_store = new CustomerDataStore();
92
93
		// Actions.
94 39
		add_action( 'pronamic_payment_status_update', array( $this, 'copy_customer_id_to_wp_user' ), 99, 1 );
95 39
	}
96
97
	/**
98
	 * Get issuers
99
	 *
100
	 * @see Core_Gateway::get_issuers()
101
	 * @return array<int, array<string, array<string>>>
102
	 */
103 3
	public function get_issuers() {
104 3
		$groups = array();
105
106
		try {
107 3
			$result = $this->client->get_issuers();
108
109
			$groups[] = array(
110
				'options' => $result,
111
			);
112 3
		} catch ( Error $e ) {
113
			// Catch Mollie error.
114 3
			$error = new \WP_Error(
115 3
				'mollie_error',
116 3
				sprintf( '%1$s (%2$s) - %3$s', $e->get_title(), $e->getCode(), $e->get_detail() )
117
			);
118
119 3
			$this->set_error( $error );
120
		} catch ( \Exception $e ) {
121
			// Catch exceptions.
122
			$error = new \WP_Error( 'mollie_error', $e->getMessage() );
123
124
			$this->set_error( $error );
125
		}
126
127 3
		return $groups;
128
	}
129
130
	/**
131
	 * Get available payment methods.
132
	 *
133
	 * @see Core_Gateway::get_available_payment_methods()
134
	 * @return array<int, string>
135
	 */
136 2
	public function get_available_payment_methods() {
137 2
		$payment_methods = array();
138
139
		// Set sequence types to get payment methods for.
140 2
		$sequence_types = array( Sequence::ONE_OFF, Sequence::RECURRING, Sequence::FIRST );
141
142 2
		$results = array();
143
144 2
		foreach ( $sequence_types as $sequence_type ) {
145
			// Get active payment methods for Mollie account.
146
			try {
147 2
				$result = $this->client->get_payment_methods( $sequence_type );
148 2
			} catch ( Error $e ) {
149
				// Catch Mollie error.
150
				$error = new \WP_Error(
151
					'mollie_error',
152
					sprintf( '%1$s (%2$s) - %3$s', $e->get_title(), $e->getCode(), $e->get_detail() )
153
				);
154
155
				$this->set_error( $error );
156
157
				break;
158 2
			} catch ( \Exception $e ) {
159
				// Catch exceptions.
160 2
				$error = new \WP_Error( 'mollie_error', $e->getMessage() );
161
162 2
				$this->set_error( $error );
163
164 2
				break;
165
			}
166
167 2
			if ( Sequence::FIRST === $sequence_type ) {
168
				foreach ( $result as $method => $title ) {
169
					unset( $result[ $method ] );
170
171
					// Get WordPress payment method for direct debit method.
172
					$method         = Methods::transform_gateway_method( $method );
173
					$payment_method = array_search( $method, PaymentMethods::get_recurring_methods(), true );
174
175
					if ( $payment_method ) {
176
						$results[ $payment_method ] = $title;
177
					}
178
				}
179
			}
180
181 2
			if ( is_array( $result ) ) {
182 2
				$results = array_merge( $results, $result );
183
			}
184
		}
185
186
		// Transform to WordPress payment methods.
187 2
		foreach ( $results as $method => $title ) {
188 2
			$method = (string) $method;
189
190 2
			$payment_method = Methods::transform_gateway_method( $method );
191
192 2
			if ( PaymentMethods::is_recurring_method( $method ) ) {
193
				$payment_method = $method;
194
			}
195
196 2
			if ( null !== $payment_method ) {
197 2
				$payment_methods[] = (string) $payment_method;
198
			}
199
		}
200
201 2
		$payment_methods = array_unique( $payment_methods );
202
203 2
		return $payment_methods;
204
	}
205
206
	/**
207
	 * Get supported payment methods
208
	 *
209
	 * @see Core_Gateway::get_supported_payment_methods()
210
	 * @return array<string>
211
	 */
212 2
	public function get_supported_payment_methods() {
213
		return array(
214 2
			PaymentMethods::APPLE_PAY,
215
			PaymentMethods::BANCONTACT,
216
			PaymentMethods::BANK_TRANSFER,
217
			PaymentMethods::BELFIUS,
218
			PaymentMethods::CREDIT_CARD,
219
			PaymentMethods::DIRECT_DEBIT,
220
			PaymentMethods::DIRECT_DEBIT_BANCONTACT,
221
			PaymentMethods::DIRECT_DEBIT_IDEAL,
222
			PaymentMethods::DIRECT_DEBIT_SOFORT,
223
			PaymentMethods::EPS,
224
			PaymentMethods::GIROPAY,
225
			PaymentMethods::IDEAL,
226
			PaymentMethods::KBC,
227
			PaymentMethods::PAYPAL,
228
			PaymentMethods::PRZELEWY24,
229
			PaymentMethods::SOFORT,
230
		);
231
	}
232
233
	/**
234
	 * Get webhook URL for Mollie.
235
	 *
236
	 * @return string|null
237
	 */
238 4
	public function get_webhook_url() {
239 4
		$url = \rest_url( Integration::REST_ROUTE_NAMESPACE . '/webhook' );
240
241 4
		$host = wp_parse_url( $url, PHP_URL_HOST );
242
243 4
		if ( is_array( $host ) ) {
244
			// Parsing failure.
245
			$host = '';
246
		}
247
248 4
		if ( 'localhost' === $host ) {
249
			// Mollie doesn't allow localhost.
250 1
			return null;
251 3
		} elseif ( '.dev' === substr( $host, -4 ) ) {
252
			// Mollie doesn't allow the .dev TLD.
253 1
			return null;
254 2
		} elseif ( '.local' === substr( $host, -6 ) ) {
255
			// Mollie doesn't allow the .local TLD.
256 1
			return null;
257 1
		} elseif ( '.test' === substr( $host, -5 ) ) {
258
			// Mollie doesn't allow the .test TLD.
259
			return null;
260
		}
261
262 1
		return $url;
263
	}
264
265
	/**
266
	 * Start
267
	 *
268
	 * @see Core_Gateway::start()
269
	 * @param Payment $payment Payment.
270
	 * @return void
271
	 * @throws \Exception Throws exception on error creating Mollie customer for payment.
272
	 */
273
	public function start( Payment $payment ) {
274
		$description = (string) $payment->get_description();
275
276
		/**
277
		 * Filters the Mollie payment description.
278
		 * 
279
		 * The maximum length of the description field differs per payment
280
		 * method, with the absolute maximum being 255 characters.
281
		 *
282
		 * @link https://docs.mollie.com/reference/v2/payments-api/create-payment#parameters
283
		 * @since 3.0.1
284
		 * @param string  $description Description.
285
		 * @param Payment $payment     Payment.
286
		 */
287
		$description = \apply_filters( 'pronamic_pay_mollie_payment_description', $description, $payment );
288
289
		$request = new PaymentRequest(
290
			AmountTransformer::transform( $payment->get_total_amount() ),
291
			$description
292
		);
293
294
		$request->redirect_url = $payment->get_return_url();
295
		$request->webhook_url  = $this->get_webhook_url();
296
297
		// Locale.
298
		$customer = $payment->get_customer();
299
300
		if ( null !== $customer ) {
301
			$request->locale = LocaleHelper::transform( $customer->get_locale() );
302
		}
303
304
		// Customer ID.
305
		$customer_id = $this->get_customer_id_for_payment( $payment );
306
307
		if ( null === $customer_id ) {
308
			$customer_id = $this->create_customer_for_payment( $payment );
309
		}
310
311
		if ( null !== $customer_id ) {
312
			$request->customer_id = $customer_id;
313
		}
314
315
		/**
316
		 * Payment method.
317
		 *
318
		 * Leap of faith if the WordPress payment method could not transform to a Mollie method?
319
		 */
320
		$payment_method = $payment->get_payment_method();
0 ignored issues
show
Bug introduced by
The method get_payment_method() does not exist on Pronamic\WordPress\Pay\Payments\Payment. ( Ignorable by Annotation )

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

320
		/** @scrutinizer ignore-call */ 
321
  $payment_method = $payment->get_payment_method();

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
321
322
		$request->set_method( Methods::transform( $payment_method, $payment_method ) );
323
324
		/**
325
		 * Sequence type.
326
		 *
327
		 * Recurring payments are created through the Payments API by providing a `sequenceType`.
328
		 */
329
		$subscriptions = $payment->get_subscriptions();
330
331
		if ( \count( $subscriptions ) > 0 ) {
332
			$request->set_method( PaymentMethods::get_first_payment_method( $payment_method ) );
333
			$request->set_sequence_type( 'first' );
334
335
			foreach ( $subscriptions as $subscription ) {
336
				$mandate_id = $subscription->get_meta( 'mollie_mandate_id' );
337
338
				if ( ! empty( $mandate_id ) ) {
339
					$request->set_method( null );
340
					$request->set_sequence_type( 'recurring' );
341
					$request->set_mandate_id( $mandate_id );
342
				}
343
			}
344
		}
345
346
		/**
347
		 * Direct Debit.
348
		 *
349
		 * Check if one-off SEPA Direct Debit can be used, otherwise short circuit payment.
350
		 */
351
		$consumer_bank_details = $payment->get_consumer_bank_details();
352
353
		if ( PaymentMethods::DIRECT_DEBIT === $payment_method && null !== $consumer_bank_details ) {
354
			$consumer_name = $consumer_bank_details->get_name();
355
			$consumer_iban = $consumer_bank_details->get_iban();
356
357
			$request->consumer_name    = $consumer_name;
358
			$request->consumer_account = $consumer_iban;
359
360
			// Check if one-off SEPA Direct Debit can be used, otherwise short circuit payment.
361
			if ( null !== $customer_id ) {
362
				// Find or create mandate.
363
				$mandate_id = $this->client->has_valid_mandate( $customer_id, PaymentMethods::DIRECT_DEBIT, $consumer_iban );
364
365
				if ( false === $mandate_id ) {
366
					$mandate = $this->client->create_mandate( $customer_id, $consumer_bank_details );
367
368
					if ( ! \property_exists( $mandate, 'id' ) ) {
369
						throw new \Exception( 'Missing mandate ID.' );
370
					}
371
372
					$mandate_id = $mandate->id;
373
				}
374
375
				// Charge immediately on-demand.
376
				$request->set_sequence_type( Sequence::RECURRING );
377
				$request->set_mandate_id( (string) $mandate_id );
378
			}
379
		}
380
381
		/**
382
		 * Metadata.
383
		 *
384
		 * Provide any data you like, for example a string or a JSON object.
385
		 * We will save the data alongside the payment. Whenever you fetch
386
		 * the payment with our API, we’ll also include the metadata. You
387
		 * can use up to approximately 1kB.
388
		 *
389
		 * @link https://docs.mollie.com/reference/v2/payments-api/create-payment
390
		 * @link https://en.wikipedia.org/wiki/Metadata
391
		 */
392
		$metadata = null;
393
394
		/**
395
		 * Filters the Mollie metadata.
396
		 *
397
		 * @since 2.2.0
398
		 *
399
		 * @param mixed   $metadata Metadata.
400
		 * @param Payment $payment  Payment.
401
		 */
402
		$metadata = \apply_filters( 'pronamic_pay_mollie_payment_metadata', $metadata, $payment );
403
404
		$request->set_metadata( $metadata );
405
406
		// Issuer.
407
		if ( Methods::IDEAL === $request->method ) {
408
			$request->issuer = $payment->get_meta( 'issuer' );
0 ignored issues
show
Documentation Bug introduced by
It seems like $payment->get_meta('issuer') can also be of type false. However, the property $issuer is declared as type null|string. Maybe add an additional type check?

Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a mixed type is assigned to a property that is type hinted more strictly.

For example, imagine you have a variable $accountId that can either hold an Id object or false (if there is no account id yet). Your code now assigns that value to the id property of an instance of the Account class. This class holds a proper account, so the id value must no longer be false.

Either this assignment is in error or a type check should be added for that assignment.

class Id
{
    public $id;

    public function __construct($id)
    {
        $this->id = $id;
    }

}

class Account
{
    /** @var  Id $id */
    public $id;
}

$account_id = false;

if (starsAreRight()) {
    $account_id = new Id(42);
}

$account = new Account();
if ($account instanceof Id)
{
    $account->id = $account_id;
}
Loading history...
409
		}
410
411
		// Billing email.
412
		$billing_email = $payment->get_email();
413
414
		/**
415
		 * Filters the Mollie payment billing email used for bank transfer payment instructions.
416
		 *
417
		 * @since 2.2.0
418
		 *
419
		 * @param string|null $billing_email Billing email.
420
		 * @param Payment     $payment       Payment.
421
		 */
422
		$billing_email = \apply_filters( 'pronamic_pay_mollie_payment_billing_email', $billing_email, $payment );
423
424
		$request->set_billing_email( $billing_email );
425
426
		// Due date.
427
		if ( ! empty( $this->config->due_date_days ) ) {
428
			try {
429
				$due_date = new DateTime( sprintf( '+%s days', $this->config->due_date_days ) );
430
			} catch ( \Exception $e ) {
431
				$due_date = null;
432
			}
433
434
			$request->set_due_date( $due_date );
435
		}
436
437
		// Create payment.
438
		$result = $this->client->create_payment( $request );
439
440
		// phpcs:disable WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase -- Mollie JSON object.
441
442
		// Set transaction ID.
443
		if ( isset( $result->id ) ) {
444
			$payment->set_transaction_id( $result->id );
445
		}
446
447
		// Set expiry date.
448
		if ( isset( $result->expiresAt ) ) {
449
			try {
450
				$expires_at = new DateTime( $result->expiresAt );
451
			} catch ( \Exception $e ) {
452
				$expires_at = null;
453
			}
454
455
			$payment->set_expiry_date( $expires_at );
456
		}
457
458
		// Set status.
459
		if ( isset( $result->status ) ) {
460
			$payment->set_status( Statuses::transform( $result->status ) );
461
		}
462
463
		// Set bank transfer recipient details.
464
		if ( isset( $result->details ) ) {
465
			$bank_transfer_recipient_details = $payment->get_bank_transfer_recipient_details();
466
467
			if ( null === $bank_transfer_recipient_details ) {
468
				$bank_transfer_recipient_details = new BankTransferDetails();
469
470
				$payment->set_bank_transfer_recipient_details( $bank_transfer_recipient_details );
471
			}
472
473
			$bank_details = $bank_transfer_recipient_details->get_bank_account();
474
475
			if ( null === $bank_details ) {
476
				$bank_details = new BankAccountDetails();
477
478
				$bank_transfer_recipient_details->set_bank_account( $bank_details );
479
			}
480
481
			$details = $result->details;
482
483
			if ( isset( $details->bankName ) ) {
484
				/**
485
				 * Set `bankName` as bank details name, as result "Stichting Mollie Payments"
486
				 * is not the name of a bank, but the account holder name.
487
				 */
488
				$bank_details->set_name( $details->bankName );
489
			}
490
491
			if ( isset( $details->bankAccount ) ) {
492
				$bank_details->set_iban( $details->bankAccount );
493
			}
494
495
			if ( isset( $details->bankBic ) ) {
496
				$bank_details->set_bic( $details->bankBic );
497
			}
498
499
			if ( isset( $details->transferReference ) ) {
500
				$bank_transfer_recipient_details->set_reference( $details->transferReference );
501
			}
502
		}
503
504
		// Handle links.
505
		if ( isset( $result->_links ) ) {
506
			$links = $result->_links;
507
508
			// Action URL.
509
			if ( isset( $links->checkout->href ) ) {
510
				$payment->set_action_url( $links->checkout->href );
511
			}
512
513
			// Change payment state URL.
514
			if ( isset( $links->changePaymentState->href ) ) {
515
				$payment->set_meta( 'mollie_change_payment_state_url', $links->changePaymentState->href );
516
			}
517
		}
518
519
		// phpcs:enable WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase -- Mollie JSON object.
520
	}
521
522
	/**
523
	 * Update status of the specified payment
524
	 *
525
	 * @param Payment $payment Payment.
526
	 * @return void
527
	 */
528
	public function update_status( Payment $payment ) {
529
		$transaction_id = $payment->get_transaction_id();
530
531
		if ( null === $transaction_id ) {
532
			return;
533
		}
534
535
		$mollie_payment = $this->client->get_payment( $transaction_id );
536
537
		$payment->set_status( Statuses::transform( $mollie_payment->get_status() ) );
538
539
		/**
540
		 * Mollie profile.
541
		 */
542
		$mollie_profile = new Profile();
543
544
		$mollie_profile->set_id( $mollie_payment->get_profile_id() );
545
546
		$profile_internal_id = $this->profile_data_store->get_or_insert_profile( $mollie_profile );
547
548
		/**
549
		 * If the Mollie payment contains a customer ID we will try to connect
550
		 * this Mollie customer ID the WordPress user and subscription.
551
		 * This can be useful in case when a WordPress user is created after
552
		 * a successful payment.
553
		 *
554
		 * @link https://www.gravityforms.com/add-ons/user-registration/
555
		 */
556
		$mollie_customer_id = $mollie_payment->get_customer_id();
557
558
		if ( null !== $mollie_customer_id ) {
559
			$mollie_customer = new Customer( $mollie_customer_id );
560
561
			$customer_internal_id = $this->customer_data_store->get_or_insert_customer(
0 ignored issues
show
Unused Code introduced by
The assignment to $customer_internal_id is dead and can be removed.
Loading history...
562
				$mollie_customer,
563
				array(
564
					'profile_id' => $profile_internal_id,
565
				),
566
				array(
567
					'profile_id' => '%s',
568
				)
569
			);
570
571
			// Meta.
572
			$customer_id = $payment->get_meta( 'mollie_customer_id' );
573
574
			if ( empty( $customer_id ) ) {
575
				$payment->set_meta( 'mollie_customer_id', $mollie_customer->get_id() );
576
			}
577
578
			// Customer.
579
			$customer = $payment->get_customer();
580
581
			if ( null !== $customer ) {
582
				// Connect to user.
583
				$user_id = $customer->get_user_id();
584
585
				if ( null !== $user_id ) {
586
					$user = \get_user_by( 'id', $user_id );
587
588
					if ( false !== $user ) {
589
						$this->customer_data_store->connect_mollie_customer_to_wp_user( $mollie_customer, $user );
590
					}
591
				}
592
			}
593
594
			// Subscription.
595
			$subscription = $payment->get_subscription();
596
597
			if ( null !== $subscription ) {
598
				$customer_id = $subscription->get_meta( 'mollie_customer_id' );
599
600
				if ( empty( $customer_id ) ) {
601
					$subscription->set_meta( 'mollie_customer_id', $mollie_customer->get_id() );
602
				}
603
604
				// Update mandate in subscription meta.
605
				$mollie_mandate_id = $mollie_payment->get_mandate_id();
606
607
				if ( null !== $mollie_mandate_id ) {
608
					$mandate_id = $subscription->get_meta( 'mollie_mandate_id' );
609
610
					// Only update if no mandate has been set yet or if payment succeeded.
611
					if ( empty( $mandate_id ) || PaymentStatus::SUCCESS === $payment->get_status() ) {
612
						$this->update_subscription_mandate( $subscription, $mollie_mandate_id, $payment->get_payment_method() );
613
					}
614
				}
615
			}
616
		}
617
618
		// phpcs:disable WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase -- Mollie JSON object.
619
		$mollie_payment_details = $mollie_payment->get_details();
620
621
		if ( null !== $mollie_payment_details ) {
622
			$consumer_bank_details = $payment->get_consumer_bank_details();
623
624
			if ( null === $consumer_bank_details ) {
625
				$consumer_bank_details = new BankAccountDetails();
626
627
				$payment->set_consumer_bank_details( $consumer_bank_details );
628
			}
629
630
			if ( isset( $mollie_payment_details->consumerName ) ) {
631
				$consumer_bank_details->set_name( $mollie_payment_details->consumerName );
632
			}
633
634
			if ( isset( $mollie_payment_details->cardHolder ) ) {
635
				$consumer_bank_details->set_name( $mollie_payment_details->cardHolder );
636
			}
637
638
			if ( isset( $mollie_payment_details->cardNumber ) ) {
639
				// The last four digits of the card number.
640
				$consumer_bank_details->set_account_number( $mollie_payment_details->cardNumber );
641
			}
642
643
			if ( isset( $mollie_payment_details->cardCountryCode ) ) {
644
				// The ISO 3166-1 alpha-2 country code of the country the card was issued in.
645
				$consumer_bank_details->set_country( $mollie_payment_details->cardCountryCode );
646
			}
647
648
			if ( isset( $mollie_payment_details->consumerAccount ) ) {
649
				switch ( $mollie_payment->get_method() ) {
650
					case Methods::BELFIUS:
651
					case Methods::DIRECT_DEBIT:
652
					case Methods::IDEAL:
653
					case Methods::KBC:
654
					case Methods::SOFORT:
655
						$consumer_bank_details->set_iban( $mollie_payment_details->consumerAccount );
656
657
						break;
658
					case Methods::BANCONTACT:
659
					case Methods::BANKTRANSFER:
660
					case Methods::PAYPAL:
661
					default:
662
						$consumer_bank_details->set_account_number( $mollie_payment_details->consumerAccount );
663
664
						break;
665
				}
666
			}
667
668
			if ( isset( $mollie_payment_details->consumerBic ) ) {
669
				$consumer_bank_details->set_bic( $mollie_payment_details->consumerBic );
670
			}
671
672
			/*
673
			 * Failure reason.
674
			 */
675
			$failure_reason = $payment->get_failure_reason();
676
677
			if ( null === $failure_reason ) {
678
				$failure_reason = new FailureReason();
679
			}
680
681
			// SEPA Direct Debit.
682
			if ( isset( $mollie_payment_details->bankReasonCode ) ) {
683
				$failure_reason->set_code( $mollie_payment_details->bankReasonCode );
684
			}
685
686
			if ( isset( $mollie_payment_details->bankReason ) ) {
687
				$failure_reason->set_message( $mollie_payment_details->bankReason );
688
			}
689
690
			// Credit card.
691
			if ( isset( $mollie_payment_details->failureReason ) ) {
692
				$failure_reason->set_code( $mollie_payment_details->failureReason );
693
			}
694
695
			if ( isset( $mollie_payment_details->failureMessage ) ) {
696
				$failure_reason->set_message( $mollie_payment_details->failureMessage );
697
			}
698
699
			$failure_code    = $failure_reason->get_code();
700
			$failure_message = $failure_reason->get_message();
701
702
			if ( ! empty( $failure_code ) || ! empty( $failure_message ) ) {
703
				$payment->set_failure_reason( $failure_reason );
704
			}
705
		}
706
707
		$links = $mollie_payment->get_links();
708
709
		// Change payment state URL.
710
		if ( \property_exists( $links, 'changePaymentState' ) ) {
711
			$payment->set_meta( 'mollie_change_payment_state_url', $links->changePaymentState->href );
712
		}
713
714
		// phpcs:enable WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase -- Mollie JSON object.
715
716
		if ( $mollie_payment->has_chargebacks() ) {
717
			$mollie_chargebacks = $this->client->get_payment_chargebacks(
718
				$mollie_payment->get_id(),
719
				array( 'limit' => 1 )
720
			);
721
722
			$mollie_chargeback = \reset( $mollie_chargebacks );
723
724
			if ( false !== $mollie_chargeback ) {
725
				$subscriptions = array_filter(
726
					$payment->get_subscriptions(),
727
					function( $subscription ) {
728
						return SubscriptionStatus::ACTIVE === $subscription->get_status();
729
					}
730
				);
731
732
				foreach ( $subscriptions as $subscription ) {
733
					if ( $mollie_chargeback->get_created_at() > $subscription->get_activated_at() ) {
734
						$subscription->set_status( SubscriptionStatus::ON_HOLD );
735
736
						$subscription->add_note(
737
							\sprintf(
738
								/* translators: 1: Mollie chargeback ID, 2: Mollie payment ID */
739
								\__( 'Subscription put on hold due to chargeback `%1$s` of payment `%2$s`.', 'pronamic_ideal' ),
740
								\esc_html( $mollie_chargeback->get_id() ),
741
								\esc_html( $mollie_payment->get_id() )
742
							)
743
						);
744
745
						$subscription->save();
746
					}
747
				}
748
			}
749
		}
750
751
		// Refunds.
752
		$amount_refunded = $mollie_payment->get_amount_refunded();
753
754
		if ( null !== $amount_refunded ) {
755
			$refunded_amount = new Money( $amount_refunded->get_value(), $amount_refunded->get_currency() );
756
757
			$payment->set_refunded_amount( $refunded_amount->get_value() > 0 ? $refunded_amount : null );
758
		}
759
	}
760
761
	/**
762
	 * Update subscription mandate.
763
	 *
764
	 * @param Subscription $subscription   Subscription.
765
	 * @param string       $mandate_id     Mollie mandate ID.
766
	 * @param string|null  $payment_method Payment method.
767
	 * @return void
768
	 * @throws \Exception Throws exception if subscription note could not be added.
769
	 */
770
	public function update_subscription_mandate( Subscription $subscription, $mandate_id, $payment_method = null ) {
771
		$customer_id = (string) $subscription->get_meta( 'mollie_customer_id' );
772
773
		$mandate = $this->client->get_mandate( $mandate_id, $customer_id );
774
775
		if ( ! \is_object( $mandate ) ) {
776
			return;
777
		}
778
779
		// Update mandate.
780
		$old_mandate_id = $subscription->get_meta( 'mollie_mandate_id' );
781
782
		$subscription->set_meta( 'mollie_mandate_id', $mandate_id );
783
784
		if ( ! empty( $old_mandate_id ) && $old_mandate_id !== $mandate_id ) {
785
			$note = \sprintf(
786
			/* translators: 1: old mandate ID, 2: new mandate ID */
787
				\__( 'Mandate for subscription changed from "%1$s" to "%2$s".', 'pronamic_ideal' ),
788
				\esc_html( $old_mandate_id ),
789
				\esc_html( $mandate_id )
790
			);
791
792
			$subscription->add_note( $note );
793
		}
794
795
		// Update payment method.
796
		$old_method = $subscription->get_payment_method();
0 ignored issues
show
Bug introduced by
The method get_payment_method() does not exist on Pronamic\WordPress\Pay\Subscriptions\Subscription. Did you maybe mean get_payments()? ( Ignorable by Annotation )

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

796
		/** @scrutinizer ignore-call */ 
797
  $old_method = $subscription->get_payment_method();

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
797
		$new_method = ( null === $payment_method && \property_exists( $mandate, 'method' ) ? Methods::transform_gateway_method( $mandate->method ) : $payment_method );
798
799
		// `Direct Debit` is not a recurring method, use `Direct Debit (mandate via ...)` instead.
800
		if ( PaymentMethods::DIRECT_DEBIT === $new_method ) {
801
			$new_method = PaymentMethods::DIRECT_DEBIT_IDEAL;
802
803
			// Use `Direct Debit (mandate via Bancontact)` if consumer account starts with `BE`.
804
			if ( \property_exists( $mandate, 'details' ) && 'BE' === \substr( $mandate->details->consumerAccount, 0, 2 ) ) {
805
				$new_method = PaymentMethods::DIRECT_DEBIT_BANCONTACT;
806
			}
807
		}
808
809
		if ( ! empty( $old_method ) && $old_method !== $new_method ) {
810
			$subscription->set_payment_method( $new_method );
0 ignored issues
show
Bug introduced by
The method set_payment_method() does not exist on Pronamic\WordPress\Pay\Subscriptions\Subscription. ( Ignorable by Annotation )

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

810
			$subscription->/** @scrutinizer ignore-call */ 
811
                  set_payment_method( $new_method );

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
811
812
			// Add note.
813
			$note = \sprintf(
814
				/* translators: 1: old payment method, 2: new payment method */
815
				\__( 'Payment method for subscription changed from "%1$s" to "%2$s".', 'pronamic_ideal' ),
816
				\esc_html( (string) PaymentMethods::get_name( $old_method ) ),
817
				\esc_html( (string) PaymentMethods::get_name( $new_method ) )
818
			);
819
820
			$subscription->add_note( $note );
821
		}
822
823
		$subscription->save();
824
	}
825
826
	/**
827
	 * Create refund.
828
	 *
829
	 * @param string $transaction_id Transaction ID.
830
	 * @param Money  $amount         Amount to refund.
831
	 * @param string $description    Refund reason.
832
	 * @return string
833
	 */
834
	public function create_refund( $transaction_id, Money $amount, $description = null ) {
835
		$request = new RefundRequest( AmountTransformer::transform( $amount ) );
836
837
		// Metadata payment ID.
838
		$payment = \get_pronamic_payment_by_transaction_id( $transaction_id );
839
840
		if ( null !== $payment ) {
841
			$request->set_metadata(
842
				array(
843
					'pronamic_payment_id' => $payment->get_id(),
844
				)
845
			);
846
		}
847
848
		// Description.
849
		if ( ! empty( $description ) ) {
850
			$request->set_description( $description );
851
		}
852
853
		$refund = $this->client->create_refund( $transaction_id, $request );
854
855
		return $refund->get_id();
856
	}
857
858
	/**
859
	 * Get Mollie customer ID for payment.
860
	 *
861
	 * @param Payment $payment Payment.
862
	 * @return string|null
863
	 */
864 10
	public function get_customer_id_for_payment( Payment $payment ) {
865 10
		$customer_ids = $this->get_customer_ids_for_payment( $payment );
866
867 10
		$customer_id = $this->get_first_existing_customer_id( $customer_ids );
868
869 10
		return $customer_id;
870
	}
871
872
	/**
873
	 * Get Mollie customers for the specified payment.
874
	 *
875
	 * @param Payment $payment Payment.
876
	 * @return array<string>
877
	 */
878 10
	private function get_customer_ids_for_payment( Payment $payment ) {
879 10
		$customer_ids = array();
880
881
		// Customer ID from subscription meta.
882 10
		$subscription = $payment->get_subscription();
883
884 10
		if ( null !== $subscription ) {
885 10
			$customer_id = $this->get_customer_id_for_subscription( $subscription );
886
887 10
			if ( null !== $customer_id ) {
888 4
				$customer_ids[] = $customer_id;
889
			}
890
		}
891
892
		// Customer ID from WordPress user.
893 10
		$customer = $payment->get_customer();
894
895 10
		if ( null !== $customer ) {
896 10
			$user_id = $customer->get_user_id();
897
898 10
			if ( ! empty( $user_id ) ) {
899 7
				$user_customer_ids = $this->get_customer_ids_for_user( $user_id );
900
901 7
				$customer_ids = \array_merge( $customer_ids, $user_customer_ids );
902
			}
903
		}
904
905 10
		return $customer_ids;
906
	}
907
908
	/**
909
	 * Get Mollie customers for the specified WordPress user ID.
910
	 *
911
	 * @param int $user_id WordPress user ID.
912
	 * @return array<string>
913
	 */
914 24
	public function get_customer_ids_for_user( $user_id ) {
915 24
		$customer_query = new CustomerQuery(
916
			array(
917 24
				'user_id' => $user_id,
918
			)
919
		);
920
921 24
		$customers = $customer_query->get_customers();
922
923 24
		$customer_ids = wp_list_pluck( $customers, 'mollie_id' );
924
925 24
		return $customer_ids;
926
	}
927
928
	/**
929
	 * Get customer ID for subscription.
930
	 *
931
	 * @param Subscription $subscription Subscription.
932
	 * @return string|null
933
	 */
934 10
	private function get_customer_id_for_subscription( Subscription $subscription ) {
935 10
		$customer_id = $subscription->get_meta( 'mollie_customer_id' );
936
937 10
		if ( empty( $customer_id ) ) {
938
			// Try to get (legacy) customer ID from first payment.
939 7
			$first_payment = $subscription->get_first_payment();
940
941 7
			if ( null !== $first_payment ) {
942 7
				$customer_id = $first_payment->get_meta( 'mollie_customer_id' );
943
			}
944
		}
945
946 10
		if ( empty( $customer_id ) ) {
947 6
			return null;
948
		}
949
950 4
		return $customer_id;
951
	}
952
953
	/**
954
	 * Get first existing customer from customers list.
955
	 *
956
	 * @param array<string> $customer_ids Customers.
957
	 * @return string|null
958
	 * @throws Error Throws error on Mollie error.
959
	 */
960 10
	private function get_first_existing_customer_id( $customer_ids ) {
961 10
		$customer_ids = \array_filter( $customer_ids );
962
963 10
		$customer_ids = \array_unique( $customer_ids );
964
965 10
		foreach ( $customer_ids as $customer_id ) {
966
			try {
967 4
				$customer = $this->client->get_customer( $customer_id );
968
			} catch ( Error $error ) {
969
				// Check for status 410 ("Gone - The customer is no longer available").
970
				if ( 410 === $error->get_status() ) {
971
					continue;
972
				}
973
974
				throw $error;
975
			}
976
977 4
			if ( null !== $customer ) {
978 4
				return $customer_id;
979
			}
980
		}
981
982 6
		return null;
983
	}
984
985
	/**
986
	 * Create customer for payment.
987
	 *
988
	 * @param Payment $payment Payment.
989
	 * @return string|null
990
	 * @throws Error Throws Error when Mollie error occurs.
991
	 * @throws \Exception Throws exception when error in customer data store occurs.
992
	 */
993
	private function create_customer_for_payment( Payment $payment ) {
994
		$mollie_customer = new Customer();
995
		$mollie_customer->set_mode( $this->config->is_test_mode() ? 'test' : 'live' );
996
		$mollie_customer->set_email( $payment->get_email() );
997
998
		$pronamic_customer = $payment->get_customer();
999
1000
		if ( null !== $pronamic_customer ) {
1001
			// Name.
1002
			$name = (string) $pronamic_customer->get_name();
1003
1004
			if ( '' !== $name ) {
1005
				$mollie_customer->set_name( $name );
1006
			}
1007
1008
			// Locale.
1009
			$locale = $pronamic_customer->get_locale();
1010
1011
			if ( null !== $locale ) {
1012
				$mollie_customer->set_locale( LocaleHelper::transform( $locale ) );
1013
			}
1014
		}
1015
1016
		// Try to get name from consumer bank details.
1017
		$consumer_bank_details = $payment->get_consumer_bank_details();
1018
1019
		if ( null === $mollie_customer->get_name() && null !== $consumer_bank_details ) {
1020
			$name = $consumer_bank_details->get_name();
1021
1022
			if ( null !== $name ) {
1023
				$mollie_customer->set_name( $name );
1024
			}
1025
		}
1026
1027
		// Create customer.
1028
		$mollie_customer = $this->client->create_customer( $mollie_customer );
1029
1030
		$customer_id = $this->customer_data_store->insert_customer( $mollie_customer );
0 ignored issues
show
Unused Code introduced by
The assignment to $customer_id is dead and can be removed.
Loading history...
1031
1032
		// Connect to user.
1033
		if ( null !== $pronamic_customer ) {
1034
			$user_id = $pronamic_customer->get_user_id();
1035
1036
			if ( null !== $user_id ) {
1037
				$user = \get_user_by( 'id', $user_id );
1038
1039
				if ( false !== $user ) {
1040
					$this->customer_data_store->connect_mollie_customer_to_wp_user( $mollie_customer, $user );
1041
				}
1042
			}
1043
		}
1044
1045
		// Store customer ID in subscription meta.
1046
		$subscription = $payment->get_subscription();
1047
1048
		if ( null !== $subscription ) {
1049
			$subscription->set_meta( 'mollie_customer_id', $mollie_customer->get_id() );
1050
		}
1051
1052
		return $mollie_customer->get_id();
1053
	}
1054
1055
	/**
1056
	 * Copy Mollie customer ID from subscription meta to WordPress user meta.
1057
	 *
1058
	 * @param Payment $payment Payment.
1059
	 * @return void
1060
	 */
1061 27
	public function copy_customer_id_to_wp_user( Payment $payment ) {
1062 27
		if ( $this->config->id !== $payment->config_id ) {
1063 1
			return;
1064
		}
1065
1066
		// Subscription.
1067 26
		$subscription = $payment->get_subscription();
1068
1069
		// Customer.
1070 26
		$customer = $payment->get_customer();
1071
1072 26
		if ( null === $customer && null !== $subscription ) {
1073 16
			$customer = $subscription->get_customer();
1074
		}
1075
1076 26
		if ( null === $customer ) {
1077
			return;
1078
		}
1079
1080
		// WordPress user.
1081 26
		$user_id = $customer->get_user_id();
1082
1083 26
		if ( null === $user_id ) {
1084 3
			return;
1085
		}
1086
1087 23
		$user = \get_user_by( 'id', $user_id );
1088
1089 23
		if ( false === $user ) {
1090 12
			return;
1091
		}
1092
1093
		// Customer IDs.
1094 11
		$customer_ids = array();
1095
1096
		// Payment.
1097 11
		$customer_ids[] = $payment->get_meta( 'mollie_customer_id' );
1098
1099
		// Subscription.
1100 11
		if ( null !== $subscription ) {
1101 11
			$customer_ids[] = $subscription->get_meta( 'mollie_customer_id' );
1102
		}
1103
1104
		// Connect.
1105 11
		$customer_ids = \array_filter( $customer_ids );
1106 11
		$customer_ids = \array_unique( $customer_ids );
1107
1108 11
		foreach ( $customer_ids as $customer_id ) {
1109 2
			$customer = new Customer( $customer_id );
1110
1111 2
			$this->customer_data_store->get_or_insert_customer( $customer );
1112
1113 2
			$this->customer_data_store->connect_mollie_customer_to_wp_user( $customer, $user );
1114
		}
1115 11
	}
1116
}
1117