Test Failed
Push — develop ( 9e96ac...fc815d )
by Reüel
06:45
created

Gateway::update_status()   F

Complexity

Conditions 45
Paths > 20000

Size

Total Lines 247
Code Lines 116

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2070

Importance

Changes 10
Bugs 0 Features 0
Metric Value
cc 45
eloc 116
c 10
b 0
f 0
nc 44237881
nop 1
dl 0
loc 247
ccs 0
cts 107
cp 0
crap 2070
rs 0

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

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
		$schedule_retry_on_error = false;
298
299
		// Locale.
300
		$customer = $payment->get_customer();
301
302
		if ( null !== $customer ) {
303
			$request->locale = LocaleHelper::transform( $customer->get_locale() );
304
		}
305
306
		// Customer ID.
307
		$customer_id = $this->get_customer_id_for_payment( $payment );
308
309
		if ( null === $customer_id ) {
310
			$customer_id = $this->create_customer_for_payment( $payment );
311
		}
312
313
		if ( null !== $customer_id ) {
314
			$request->customer_id = $customer_id;
315
		}
316
317
		/**
318
		 * Payment method.
319
		 *
320
		 * Leap of faith if the WordPress payment method could not transform to a Mollie method?
321
		 */
322
		$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

322
		/** @scrutinizer ignore-call */ 
323
  $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...
323
324
		$request->set_method( Methods::transform( $payment_method, $payment_method ) );
325
326
		/**
327
		 * Sequence type.
328
		 *
329
		 * Recurring payments are created through the Payments API by providing a `sequenceType`.
330
		 */
331
		$subscriptions = $payment->get_subscriptions();
332
333
		if ( \count( $subscriptions ) > 0 ) {
334
			$first_method = PaymentMethods::get_first_payment_method( $payment_method );
335
336
			$request->set_method( Methods::transform( $first_method, $first_method ) );
337
338
			$request->set_sequence_type( 'first' );
339
340
			foreach ( $subscriptions as $subscription ) {
341
				$mandate_id = $subscription->get_meta( 'mollie_mandate_id' );
342
343
				if ( ! empty( $mandate_id ) ) {
344
					$request->set_method( null );
345
					$request->set_sequence_type( 'recurring' );
346
					$request->set_mandate_id( $mandate_id );
347
348
					$schedule_retry_on_error = true;
349
				}
350
			}
351
		}
352
353
		/**
354
		 * Direct Debit.
355
		 *
356
		 * Check if one-off SEPA Direct Debit can be used, otherwise short circuit payment.
357
		 */
358
		$consumer_bank_details = $payment->get_consumer_bank_details();
359
360
		if ( PaymentMethods::DIRECT_DEBIT === $payment_method && null !== $consumer_bank_details ) {
361
			$consumer_name = $consumer_bank_details->get_name();
362
			$consumer_iban = $consumer_bank_details->get_iban();
363
364
			$request->consumer_name    = $consumer_name;
365
			$request->consumer_account = $consumer_iban;
366
367
			// Check if one-off SEPA Direct Debit can be used, otherwise short circuit payment.
368
			if ( null !== $customer_id ) {
369
				// Find or create mandate.
370
				$mandate_id = $this->client->has_valid_mandate( $customer_id, PaymentMethods::DIRECT_DEBIT, $consumer_iban );
371
372
				if ( false === $mandate_id ) {
373
					$mandate = $this->client->create_mandate( $customer_id, $consumer_bank_details );
374
375
					if ( ! \property_exists( $mandate, 'id' ) ) {
376
						throw new \Exception( 'Missing mandate ID.' );
377
					}
378
379
					$mandate_id = $mandate->id;
380
				}
381
382
				// Charge immediately on-demand.
383
				$request->set_sequence_type( Sequence::RECURRING );
384
				$request->set_mandate_id( (string) $mandate_id );
385
			}
386
		}
387
388
		/**
389
		 * Metadata.
390
		 *
391
		 * Provide any data you like, for example a string or a JSON object.
392
		 * We will save the data alongside the payment. Whenever you fetch
393
		 * the payment with our API, we’ll also include the metadata. You
394
		 * can use up to approximately 1kB.
395
		 *
396
		 * @link https://docs.mollie.com/reference/v2/payments-api/create-payment
397
		 * @link https://en.wikipedia.org/wiki/Metadata
398
		 */
399
		$metadata = null;
400
401
		/**
402
		 * Filters the Mollie metadata.
403
		 *
404
		 * @since 2.2.0
405
		 *
406
		 * @param mixed   $metadata Metadata.
407
		 * @param Payment $payment  Payment.
408
		 */
409
		$metadata = \apply_filters( 'pronamic_pay_mollie_payment_metadata', $metadata, $payment );
410
411
		$request->set_metadata( $metadata );
412
413
		// Issuer.
414
		if ( Methods::IDEAL === $request->method ) {
415
			$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...
416
		}
417
418
		// Billing email.
419
		$billing_email = $payment->get_email();
420
421
		/**
422
		 * Filters the Mollie payment billing email used for bank transfer payment instructions.
423
		 *
424
		 * @since 2.2.0
425
		 *
426
		 * @param string|null $billing_email Billing email.
427
		 * @param Payment     $payment       Payment.
428
		 */
429
		$billing_email = \apply_filters( 'pronamic_pay_mollie_payment_billing_email', $billing_email, $payment );
430
431
		$request->set_billing_email( $billing_email );
432
433
		// Due date.
434
		if ( ! empty( $this->config->due_date_days ) ) {
435
			try {
436
				$due_date = new DateTime( sprintf( '+%s days', $this->config->due_date_days ) );
437
			} catch ( \Exception $e ) {
438
				$due_date = null;
439
			}
440
441
			$request->set_due_date( $due_date );
442
		}
443
444
		// Create payment.
445
		try {
446
			$result = $this->client->create_payment( $request );
447
		} catch ( \Exception $e ) {
448
			$code = $e->getCode();
449
450
			/*
451
			 * Only schedule retry for specific status codes.
452
			 *
453
			 * @link https://docs.mollie.com/overview/handling-errors
454
			 */
455
			if ( \in_array( $code, array( 429, 502, 503 ), true ) ) {
456
				$schedule_retry_on_error = false;
457
			}
458
459
			if ( $schedule_retry_on_error ) {
460
				\as_schedule_single_action(
0 ignored issues
show
Bug introduced by
The function as_schedule_single_action was not found. Maybe you did not declare it correctly or list all dependencies? ( Ignorable by Annotation )

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

460
				/** @scrutinizer ignore-call */ 
461
    \as_schedule_single_action(
Loading history...
461
					\time() + 60,
462
					'pronamic_pay_mollie_payment_start',
463
					array(
464
						'payment_id' => $payment->get_id(),
465
					),
466
					'pronamic-pay-mollie'
467
				);
468
469
				// Add note.
470
				$error_code = ( $code > 0 ? sprintf( '%s: ', $code ) : '' );
471
472
				$payment->add_note( $error_code . $e->getMessage() );
473
474
				return;
475
			}
476
477
			// Rethrow exception.
478
			throw $e;
479
		}
480
481
		// phpcs:disable WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase -- Mollie JSON object.
482
483
		// Set transaction ID.
484
		if ( isset( $result->id ) ) {
485
			$payment->set_transaction_id( $result->id );
486
		}
487
488
		// Set expiry date.
489
		if ( isset( $result->expiresAt ) ) {
490
			try {
491
				$expires_at = new DateTime( $result->expiresAt );
492
			} catch ( \Exception $e ) {
493
				$expires_at = null;
494
			}
495
496
			$payment->set_expiry_date( $expires_at );
497
		}
498
499
		// Set status.
500
		if ( isset( $result->status ) ) {
501
			$payment->set_status( Statuses::transform( $result->status ) );
502
		}
503
504
		// Set bank transfer recipient details.
505
		if ( isset( $result->details ) ) {
506
			$bank_transfer_recipient_details = $payment->get_bank_transfer_recipient_details();
507
508
			if ( null === $bank_transfer_recipient_details ) {
509
				$bank_transfer_recipient_details = new BankTransferDetails();
510
511
				$payment->set_bank_transfer_recipient_details( $bank_transfer_recipient_details );
512
			}
513
514
			$bank_details = $bank_transfer_recipient_details->get_bank_account();
515
516
			if ( null === $bank_details ) {
517
				$bank_details = new BankAccountDetails();
518
519
				$bank_transfer_recipient_details->set_bank_account( $bank_details );
520
			}
521
522
			$details = $result->details;
523
524
			if ( isset( $details->bankName ) ) {
525
				/**
526
				 * Set `bankName` as bank details name, as result "Stichting Mollie Payments"
527
				 * is not the name of a bank, but the account holder name.
528
				 */
529
				$bank_details->set_name( $details->bankName );
530
			}
531
532
			if ( isset( $details->bankAccount ) ) {
533
				$bank_details->set_iban( $details->bankAccount );
534
			}
535
536
			if ( isset( $details->bankBic ) ) {
537
				$bank_details->set_bic( $details->bankBic );
538
			}
539
540
			if ( isset( $details->transferReference ) ) {
541
				$bank_transfer_recipient_details->set_reference( $details->transferReference );
542
			}
543
		}
544
545
		// Handle links.
546
		if ( isset( $result->_links ) ) {
547
			$links = $result->_links;
548
549
			// Action URL.
550
			if ( isset( $links->checkout->href ) ) {
551
				$payment->set_action_url( $links->checkout->href );
552
			}
553
554
			// Change payment state URL.
555
			if ( isset( $links->changePaymentState->href ) ) {
556
				$payment->set_meta( 'mollie_change_payment_state_url', $links->changePaymentState->href );
557
			}
558
		}
559
560
		// phpcs:enable WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase -- Mollie JSON object.
561
	}
562
563
	/**
564
	 * Update status of the specified payment
565
	 *
566
	 * @param Payment $payment Payment.
567
	 * @return void
568
	 */
569
	public function update_status( Payment $payment ) {
570
		$transaction_id = $payment->get_transaction_id();
571
572
		if ( null === $transaction_id ) {
573
			return;
574
		}
575
576
		$mollie_payment = $this->client->get_payment( $transaction_id );
577
578
		$payment->set_status( Statuses::transform( $mollie_payment->get_status() ) );
579
580
		/**
581
		 * Mollie profile.
582
		 */
583
		$mollie_profile = new Profile();
584
585
		$mollie_profile->set_id( $mollie_payment->get_profile_id() );
586
587
		$profile_internal_id = $this->profile_data_store->get_or_insert_profile( $mollie_profile );
588
589
		/**
590
		 * If the Mollie payment contains a customer ID we will try to connect
591
		 * this Mollie customer ID the WordPress user and subscription.
592
		 * This can be useful in case when a WordPress user is created after
593
		 * a successful payment.
594
		 *
595
		 * @link https://www.gravityforms.com/add-ons/user-registration/
596
		 */
597
		$mollie_customer_id = $mollie_payment->get_customer_id();
598
599
		if ( null !== $mollie_customer_id ) {
600
			$mollie_customer = new Customer( $mollie_customer_id );
601
602
			$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...
603
				$mollie_customer,
604
				array(
605
					'profile_id' => $profile_internal_id,
606
				),
607
				array(
608
					'profile_id' => '%s',
609
				)
610
			);
611
612
			// Customer.
613
			$customer = $payment->get_customer();
614
615
			if ( null !== $customer ) {
616
				// Connect to user.
617
				$user_id = $customer->get_user_id();
618
619
				if ( null !== $user_id ) {
620
					$user = \get_user_by( 'id', $user_id );
621
622
					if ( false !== $user ) {
623
						$this->customer_data_store->connect_mollie_customer_to_wp_user( $mollie_customer, $user );
624
					}
625
				}
626
			}
627
		}
628
629
		/**
630
		 * Customer ID.
631
		 */
632
		$mollie_customer_id = $mollie_payment->get_customer_id();
633
634
		if ( null !== $mollie_customer_id ) {
635
			$customer_id = $payment->get_meta( 'mollie_customer_id' );
636
637
			if ( empty( $customer_id ) ) {
638
				$payment->set_meta( 'mollie_customer_id', $mollie_customer_id );
639
			}
640
641
			foreach ( $payment->get_subscriptions() as $subscription ) {
642
				$customer_id = $subscription->get_meta( 'mollie_customer_id' );
643
644
				if ( empty( $customer_id ) ) {
645
					$subscription->set_meta( 'mollie_customer_id', $mollie_customer_id );
646
				}
647
			}
648
		}
649
650
		/**
651
		 * Mandate ID.
652
		 */
653
		$mollie_mandate_id = $mollie_payment->get_mandate_id();
654
655
		if ( null !== $mollie_mandate_id ) {
656
			$mandate_id = $payment->get_meta( 'mollie_mandate_id' );
657
658
			if ( empty( $mandate_id ) ) {
659
				$payment->set_meta( 'mollie_mandate_id', $mollie_mandate_id );
660
			}
661
662
			foreach ( $payment->get_subscriptions() as $subscription ) {
663
				$mandate_id = $subscription->get_meta( 'mollie_mandate_id' );
664
665
				if ( empty( $mandate_id ) ) {
666
					$subscription->set_meta( 'mollie_mandate_id', $mollie_mandate_id );
667
				}
668
			}
669
		}
670
671
		// phpcs:disable WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase -- Mollie JSON object.
672
		$mollie_payment_details = $mollie_payment->get_details();
673
674
		if ( null !== $mollie_payment_details ) {
675
			$consumer_bank_details = $payment->get_consumer_bank_details();
676
677
			if ( null === $consumer_bank_details ) {
678
				$consumer_bank_details = new BankAccountDetails();
679
680
				$payment->set_consumer_bank_details( $consumer_bank_details );
681
			}
682
683
			if ( isset( $mollie_payment_details->consumerName ) ) {
684
				$consumer_bank_details->set_name( $mollie_payment_details->consumerName );
685
			}
686
687
			if ( isset( $mollie_payment_details->cardHolder ) ) {
688
				$consumer_bank_details->set_name( $mollie_payment_details->cardHolder );
689
			}
690
691
			if ( isset( $mollie_payment_details->cardNumber ) ) {
692
				// The last four digits of the card number.
693
				$consumer_bank_details->set_account_number( $mollie_payment_details->cardNumber );
694
			}
695
696
			if ( isset( $mollie_payment_details->cardCountryCode ) ) {
697
				// The ISO 3166-1 alpha-2 country code of the country the card was issued in.
698
				$consumer_bank_details->set_country( $mollie_payment_details->cardCountryCode );
699
			}
700
701
			if ( isset( $mollie_payment_details->consumerAccount ) ) {
702
				switch ( $mollie_payment->get_method() ) {
703
					case Methods::BELFIUS:
704
					case Methods::DIRECT_DEBIT:
705
					case Methods::IDEAL:
706
					case Methods::KBC:
707
					case Methods::SOFORT:
708
						$consumer_bank_details->set_iban( $mollie_payment_details->consumerAccount );
709
710
						break;
711
					case Methods::BANCONTACT:
712
					case Methods::BANKTRANSFER:
713
					case Methods::PAYPAL:
714
					default:
715
						$consumer_bank_details->set_account_number( $mollie_payment_details->consumerAccount );
716
717
						break;
718
				}
719
			}
720
721
			if ( isset( $mollie_payment_details->consumerBic ) ) {
722
				$consumer_bank_details->set_bic( $mollie_payment_details->consumerBic );
723
			}
724
725
			/*
726
			 * Failure reason.
727
			 */
728
			$failure_reason = $payment->get_failure_reason();
729
730
			if ( null === $failure_reason ) {
731
				$failure_reason = new FailureReason();
732
			}
733
734
			// SEPA Direct Debit.
735
			if ( isset( $mollie_payment_details->bankReasonCode ) ) {
736
				$failure_reason->set_code( $mollie_payment_details->bankReasonCode );
737
			}
738
739
			if ( isset( $mollie_payment_details->bankReason ) ) {
740
				$failure_reason->set_message( $mollie_payment_details->bankReason );
741
			}
742
743
			// Credit card.
744
			if ( isset( $mollie_payment_details->failureReason ) ) {
745
				$failure_reason->set_code( $mollie_payment_details->failureReason );
746
			}
747
748
			if ( isset( $mollie_payment_details->failureMessage ) ) {
749
				$failure_reason->set_message( $mollie_payment_details->failureMessage );
750
			}
751
752
			$failure_code    = $failure_reason->get_code();
753
			$failure_message = $failure_reason->get_message();
754
755
			if ( ! empty( $failure_code ) || ! empty( $failure_message ) ) {
756
				$payment->set_failure_reason( $failure_reason );
757
			}
758
		}
759
760
		$links = $mollie_payment->get_links();
761
762
		// Change payment state URL.
763
		if ( \property_exists( $links, 'changePaymentState' ) ) {
764
			$payment->set_meta( 'mollie_change_payment_state_url', $links->changePaymentState->href );
765
		}
766
767
		// phpcs:enable WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase -- Mollie JSON object.
768
769
		if ( $mollie_payment->has_chargebacks() ) {
770
			$mollie_chargebacks = $this->client->get_payment_chargebacks(
771
				$mollie_payment->get_id(),
772
				array( 'limit' => 1 )
773
			);
774
775
			$mollie_chargeback = \reset( $mollie_chargebacks );
776
777
			if ( false !== $mollie_chargeback ) {
778
				$subscriptions = array_filter(
779
					$payment->get_subscriptions(),
780
					function ( $subscription ) {
781
						return SubscriptionStatus::ACTIVE === $subscription->get_status();
782
					}
783
				);
784
785
				foreach ( $subscriptions as $subscription ) {
786
					if ( $mollie_chargeback->get_created_at() > $subscription->get_activated_at() ) {
787
						$subscription->set_status( SubscriptionStatus::ON_HOLD );
788
789
						$subscription->add_note(
790
							\sprintf(
791
								/* translators: 1: Mollie chargeback ID, 2: Mollie payment ID */
792
								\__( 'Subscription put on hold due to chargeback `%1$s` of payment `%2$s`.', 'pronamic_ideal' ),
793
								\esc_html( $mollie_chargeback->get_id() ),
794
								\esc_html( $mollie_payment->get_id() )
795
							)
796
						);
797
					}
798
				}
799
			}
800
		}
801
802
		// Refunds.
803
		$amount_refunded = $mollie_payment->get_amount_refunded();
804
805
		if ( null !== $amount_refunded ) {
806
			$refunded_amount = new Money( $amount_refunded->get_value(), $amount_refunded->get_currency() );
807
808
			$payment->set_refunded_amount( $refunded_amount->get_value() > 0 ? $refunded_amount : null );
809
		}
810
811
		// Save.
812
		$payment->save();
813
814
		foreach ( $payment->get_subscriptions() as $subscription ) {
815
			$subscription->save();
816
		}
817
	}
818
819
	/**
820
	 * Update subscription mandate.
821
	 *
822
	 * @param Subscription $subscription   Subscription.
823
	 * @param string       $mandate_id     Mollie mandate ID.
824
	 * @param string|null  $payment_method Payment method.
825
	 * @return void
826
	 * @throws \Exception Throws exception if subscription note could not be added.
827
	 */
828
	public function update_subscription_mandate( Subscription $subscription, $mandate_id, $payment_method = null ) {
829
		$customer_id = (string) $subscription->get_meta( 'mollie_customer_id' );
830
831
		$mandate = $this->client->get_mandate( $mandate_id, $customer_id );
832
833
		if ( ! \is_object( $mandate ) ) {
834
			return;
835
		}
836
837
		// Update mandate.
838
		$old_mandate_id = $subscription->get_meta( 'mollie_mandate_id' );
839
840
		$subscription->set_meta( 'mollie_mandate_id', $mandate_id );
841
842
		if ( ! empty( $old_mandate_id ) && $old_mandate_id !== $mandate_id ) {
843
			$note = \sprintf(
844
			/* translators: 1: old mandate ID, 2: new mandate ID */
845
				\__( 'Mandate for subscription changed from "%1$s" to "%2$s".', 'pronamic_ideal' ),
846
				\esc_html( $old_mandate_id ),
847
				\esc_html( $mandate_id )
848
			);
849
850
			$subscription->add_note( $note );
851
		}
852
853
		// Update payment method.
854
		$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

854
		/** @scrutinizer ignore-call */ 
855
  $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...
855
		$new_method = ( null === $payment_method && \property_exists( $mandate, 'method' ) ? Methods::transform_gateway_method( $mandate->method ) : $payment_method );
856
857
		// `Direct Debit` is not a recurring method, use `Direct Debit (mandate via ...)` instead.
858
		if ( PaymentMethods::DIRECT_DEBIT === $new_method ) {
859
			$new_method = PaymentMethods::DIRECT_DEBIT_IDEAL;
860
861
			// Use `Direct Debit (mandate via Bancontact)` if consumer account starts with `BE`.
862
			if ( \property_exists( $mandate, 'details' ) && 'BE' === \substr( $mandate->details->consumerAccount, 0, 2 ) ) {
863
				$new_method = PaymentMethods::DIRECT_DEBIT_BANCONTACT;
864
			}
865
		}
866
867
		if ( ! empty( $old_method ) && $old_method !== $new_method ) {
868
			$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

868
			$subscription->/** @scrutinizer ignore-call */ 
869
                  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...
869
870
			// Add note.
871
			$note = \sprintf(
872
				/* translators: 1: old payment method, 2: new payment method */
873
				\__( 'Payment method for subscription changed from "%1$s" to "%2$s".', 'pronamic_ideal' ),
874
				\esc_html( (string) PaymentMethods::get_name( $old_method ) ),
875
				\esc_html( (string) PaymentMethods::get_name( $new_method ) )
876
			);
877
878
			$subscription->add_note( $note );
879
		}
880
881 10
		$subscription->save();
882 10
	}
883
884 10
	/**
885
	 * Create refund.
886 10
	 *
887
	 * @param string $transaction_id Transaction ID.
888
	 * @param Money  $amount         Amount to refund.
889
	 * @param string $description    Refund reason.
890
	 * @return string
891
	 */
892
	public function create_refund( $transaction_id, Money $amount, $description = null ) {
893
		$request = new RefundRequest( AmountTransformer::transform( $amount ) );
894
895 10
		// Metadata payment ID.
896 10
		$payment = \get_pronamic_payment_by_transaction_id( $transaction_id );
897
898
		if ( null !== $payment ) {
899 10
			$request->set_metadata(
900
				array(
901 10
					'pronamic_payment_id' => $payment->get_id(),
902 10
				)
903
			);
904 10
		}
905 4
906
		// Description.
907
		if ( ! empty( $description ) ) {
908
			$request->set_description( $description );
909
		}
910 10
911
		$refund = $this->client->create_refund( $transaction_id, $request );
912 10
913 10
		return $refund->get_id();
914
	}
915 10
916 7
	/**
917
	 * Get Mollie customer ID for payment.
918 7
	 *
919
	 * @param Payment $payment Payment.
920
	 * @return string|null
921
	 */
922 10
	public function get_customer_id_for_payment( Payment $payment ) {
923
		$customer_ids = $this->get_customer_ids_for_payment( $payment );
924
925
		$customer_id = $this->get_first_existing_customer_id( $customer_ids );
926
927
		return $customer_id;
928
	}
929
930
	/**
931 24
	 * Get Mollie customers for the specified payment.
932 24
	 *
933
	 * @param Payment $payment Payment.
934 24
	 * @return array<string>
935
	 */
936
	private function get_customer_ids_for_payment( Payment $payment ) {
937
		$customer_ids = array();
938 24
939
		// Customer ID from subscription meta.
940 24
		$subscription = $payment->get_subscription();
941
942 24
		if ( null !== $subscription ) {
943
			$customer_id = $this->get_customer_id_for_subscription( $subscription );
944
945
			if ( null !== $customer_id ) {
946
				$customer_ids[] = $customer_id;
947
			}
948
		}
949
950
		// Customer ID from WordPress user.
951 10
		$customer = $payment->get_customer();
952 10
953
		if ( null !== $customer ) {
954 10
			$user_id = $customer->get_user_id();
955
956 7
			if ( ! empty( $user_id ) ) {
957
				$user_customer_ids = $this->get_customer_ids_for_user( $user_id );
958 7
959 7
				$customer_ids = \array_merge( $customer_ids, $user_customer_ids );
960
			}
961
		}
962
963 10
		return $customer_ids;
964 6
	}
965
966
	/**
967 4
	 * Get Mollie customers for the specified WordPress user ID.
968
	 *
969
	 * @param int $user_id WordPress user ID.
970
	 * @return array<string>
971
	 */
972
	public function get_customer_ids_for_user( $user_id ) {
973
		$customer_query = new CustomerQuery(
974
			array(
975
				'user_id' => $user_id,
976
			)
977 10
		);
978 10
979
		$customers = $customer_query->get_customers();
980 10
981
		$customer_ids = wp_list_pluck( $customers, 'mollie_id' );
982 10
983
		return $customer_ids;
984 4
	}
985
986
	/**
987
	 * Get customer ID for subscription.
988
	 *
989
	 * @param Subscription $subscription Subscription.
990
	 * @return string|null
991
	 */
992
	private function get_customer_id_for_subscription( Subscription $subscription ) {
993
		$customer_id = $subscription->get_meta( 'mollie_customer_id' );
994 4
995 4
		if ( empty( $customer_id ) ) {
996
			// Try to get (legacy) customer ID from first payment.
997
			$first_payment = $subscription->get_first_payment();
998
999 6
			if ( null !== $first_payment ) {
1000
				$customer_id = $first_payment->get_meta( 'mollie_customer_id' );
1001
			}
1002
		}
1003
1004
		if ( empty( $customer_id ) ) {
1005
			return null;
1006
		}
1007
1008
		return $customer_id;
1009
	}
1010
1011
	/**
1012
	 * Get first existing customer from customers list.
1013
	 *
1014
	 * @param array<string> $customer_ids Customers.
1015
	 * @return string|null
1016
	 * @throws Error Throws error on Mollie error.
1017
	 */
1018
	private function get_first_existing_customer_id( $customer_ids ) {
1019
		$customer_ids = \array_filter( $customer_ids );
1020
1021
		$customer_ids = \array_unique( $customer_ids );
1022
1023
		foreach ( $customer_ids as $customer_id ) {
1024
			try {
1025
				$customer = $this->client->get_customer( $customer_id );
1026
			} catch ( Error $error ) {
1027
				// Check for status 410 ("Gone - The customer is no longer available").
1028
				if ( 410 === $error->get_status() ) {
1029
					continue;
1030
				}
1031
1032
				throw $error;
1033
			}
1034
1035
			if ( null !== $customer ) {
1036
				return $customer_id;
1037
			}
1038
		}
1039
1040
		return null;
1041
	}
1042
1043
	/**
1044
	 * Create customer for payment.
1045
	 *
1046
	 * @param Payment $payment Payment.
1047
	 * @return string|null
1048
	 * @throws Error Throws Error when Mollie error occurs.
1049
	 * @throws \Exception Throws exception when error in customer data store occurs.
1050
	 */
1051
	private function create_customer_for_payment( Payment $payment ) {
1052
		$mollie_customer = new Customer();
1053
		$mollie_customer->set_mode( $this->config->is_test_mode() ? 'test' : 'live' );
1054
		$mollie_customer->set_email( $payment->get_email() );
1055
1056
		$pronamic_customer = $payment->get_customer();
1057
1058
		if ( null !== $pronamic_customer ) {
1059
			// Name.
1060
			$name = (string) $pronamic_customer->get_name();
1061
1062
			if ( '' !== $name ) {
1063
				$mollie_customer->set_name( $name );
1064
			}
1065
1066
			// Locale.
1067
			$locale = $pronamic_customer->get_locale();
1068
1069
			if ( null !== $locale ) {
1070
				$mollie_customer->set_locale( LocaleHelper::transform( $locale ) );
1071
			}
1072
		}
1073
1074
		// Try to get name from consumer bank details.
1075
		$consumer_bank_details = $payment->get_consumer_bank_details();
1076
1077
		if ( null === $mollie_customer->get_name() && null !== $consumer_bank_details ) {
1078 27
			$name = $consumer_bank_details->get_name();
1079 27
1080 1
			if ( null !== $name ) {
1081
				$mollie_customer->set_name( $name );
1082
			}
1083
		}
1084 26
1085
		// Create customer.
1086
		$mollie_customer = $this->client->create_customer( $mollie_customer );
1087 26
1088
		$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...
1089 26
1090 16
		// Connect to user.
1091
		if ( null !== $pronamic_customer ) {
1092
			$user_id = $pronamic_customer->get_user_id();
1093 26
1094
			if ( null !== $user_id ) {
1095
				$user = \get_user_by( 'id', $user_id );
1096
1097
				if ( false !== $user ) {
1098 26
					$this->customer_data_store->connect_mollie_customer_to_wp_user( $mollie_customer, $user );
1099
				}
1100 26
			}
1101 3
		}
1102
1103
		// Store customer ID in subscription meta.
1104 23
		$subscription = $payment->get_subscription();
1105
1106 23
		if ( null !== $subscription ) {
1107 12
			$subscription->set_meta( 'mollie_customer_id', $mollie_customer->get_id() );
1108
		}
1109
1110
		return $mollie_customer->get_id();
1111 11
	}
1112
1113
	/**
1114 11
	 * Copy Mollie customer ID from subscription meta to WordPress user meta.
1115
	 *
1116
	 * @param Payment $payment Payment.
1117 11
	 * @return void
1118 11
	 */
1119
	public function copy_customer_id_to_wp_user( Payment $payment ) {
1120
		if ( $this->config->id !== $payment->config_id ) {
1121
			return;
1122 11
		}
1123 11
1124
		// Subscription.
1125 11
		$subscription = $payment->get_subscription();
1126 2
1127
		// Customer.
1128 2
		$customer = $payment->get_customer();
1129
1130 2
		if ( null === $customer && null !== $subscription ) {
1131
			$customer = $subscription->get_customer();
1132 11
		}
1133
1134
		if ( null === $customer ) {
1135
			return;
1136
		}
1137
1138
		// WordPress user.
1139
		$user_id = $customer->get_user_id();
1140
1141
		if ( null === $user_id ) {
1142
			return;
1143
		}
1144
1145
		$user = \get_user_by( 'id', $user_id );
1146
1147
		if ( false === $user ) {
1148
			return;
1149
		}
1150
1151
		// Customer IDs.
1152
		$customer_ids = array();
1153
1154
		// Payment.
1155
		$customer_ids[] = $payment->get_meta( 'mollie_customer_id' );
1156
1157
		// Subscription.
1158
		if ( null !== $subscription ) {
1159
			$customer_ids[] = $subscription->get_meta( 'mollie_customer_id' );
1160
		}
1161
1162
		// Connect.
1163
		$customer_ids = \array_filter( $customer_ids );
1164
		$customer_ids = \array_unique( $customer_ids );
1165
1166
		foreach ( $customer_ids as $customer_id ) {
1167
			$customer = new Customer( $customer_id );
1168
1169
			$this->customer_data_store->get_or_insert_customer( $customer );
1170
1171
			$this->customer_data_store->connect_mollie_customer_to_wp_user( $customer, $user );
1172
		}
1173
	}
1174
}
1175