Test Failed
Push — develop ( fc815d...ebfe7f )
by Remco
04:45
created

Gateway::copy_customer_id_to_wp_user()   B

Complexity

Conditions 9
Paths 15

Size

Total Lines 53
Code Lines 24

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 90

Importance

Changes 3
Bugs 0 Features 0
Metric Value
cc 9
eloc 24
nc 15
nop 1
dl 0
loc 53
ccs 0
cts 0
cp 0
crap 90
rs 8.0555
c 3
b 0
f 0

How to fix   Long Method   

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

449
			$payment->/** @scrutinizer ignore-call */ 
450
             delete_meta( 'mollie_create_payment_attempt' );

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...
450
		} catch ( Error $error ) {
451
			if ( 'recurring' !== $request->get_sequence_type() ) {
452
				throw $error;
453
			}
454
455
			if ( null === $request->get_mandate_id() ) {
456
				throw $error;
457
			}
458
459
			/**
460
			 * Only schedule retry for specific status codes.
461
			 *
462
			 * @link https://docs.mollie.com/overview/handling-errors
463
			 */
464
			if ( ! \in_array( $error->get_status(), array( 429, 502, 503 ), true ) ) {
465
				throw $error;
466
			}
467
468
			\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

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

880
		/** @scrutinizer ignore-call */ 
881
  $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...
881 10
		$new_method = ( null === $payment_method && \property_exists( $mandate, 'method' ) ? Methods::transform_gateway_method( $mandate->method ) : $payment_method );
882 10
883
		// `Direct Debit` is not a recurring method, use `Direct Debit (mandate via ...)` instead.
884 10
		if ( PaymentMethods::DIRECT_DEBIT === $new_method ) {
885
			$new_method = PaymentMethods::DIRECT_DEBIT_IDEAL;
886 10
887
			// Use `Direct Debit (mandate via Bancontact)` if consumer account starts with `BE`.
888
			if ( \property_exists( $mandate, 'details' ) && 'BE' === \substr( $mandate->details->consumerAccount, 0, 2 ) ) {
889
				$new_method = PaymentMethods::DIRECT_DEBIT_BANCONTACT;
890
			}
891
		}
892
893
		if ( ! empty( $old_method ) && $old_method !== $new_method ) {
894
			$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

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