Test Failed
Push — develop ( 4e8f2d...c0ae3c )
by Reüel
04:48
created

src/Gateway.php (9 issues)

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
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 Error Mollie error.
272
	 * @throws \Exception Throws exception on error creating Mollie customer for payment.
273
	 */
274
	public function start( Payment $payment ) {
275
		$description = (string) $payment->get_description();
276
277
		/**
278
		 * Filters the Mollie payment description.
279
		 * 
280
		 * The maximum length of the description field differs per payment
281
		 * method, with the absolute maximum being 255 characters.
282
		 *
283
		 * @link https://docs.mollie.com/reference/v2/payments-api/create-payment#parameters
284
		 * @since 3.0.1
285
		 * @param string  $description Description.
286
		 * @param Payment $payment     Payment.
287
		 */
288
		$description = \apply_filters( 'pronamic_pay_mollie_payment_description', $description, $payment );
289
290
		$request = new PaymentRequest(
291
			AmountTransformer::transform( $payment->get_total_amount() ),
292
			$description
293
		);
294
295
		$request->redirect_url = $payment->get_return_url();
296
		$request->webhook_url  = $this->get_webhook_url();
297
298
		// Locale.
299
		$customer = $payment->get_customer();
300
301
		if ( null !== $customer ) {
302
			$request->locale = LocaleHelper::transform( $customer->get_locale() );
303
		}
304
305
		// Customer ID.
306
		$customer_id = $this->get_customer_id_for_payment( $payment );
307
308
		if ( null === $customer_id ) {
309
			$customer_id = $this->create_customer_for_payment( $payment );
310
		}
311
312
		if ( null !== $customer_id ) {
313
			$request->customer_id = $customer_id;
314
		}
315
316
		/**
317
		 * Payment method.
318
		 *
319
		 * Leap of faith if the WordPress payment method could not transform to a Mollie method?
320
		 */
321
		$payment_method = $payment->get_payment_method();
0 ignored issues
show
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

321
		/** @scrutinizer ignore-call */ 
322
  $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...
322
323
		$request->set_method( Methods::transform( $payment_method, $payment_method ) );
324
325
		/**
326
		 * Sequence type.
327
		 *
328
		 * Recurring payments are created through the Payments API by providing a `sequenceType`.
329
		 */
330
		$subscriptions = $payment->get_subscriptions();
331
332
		if ( \count( $subscriptions ) > 0 ) {
333
			$first_method = PaymentMethods::get_first_payment_method( $payment_method );
334
335
			$request->set_method( Methods::transform( $first_method, $first_method ) );
336
337
			$request->set_sequence_type( 'first' );
338
339
			foreach ( $subscriptions as $subscription ) {
340
				$mandate_id = $subscription->get_meta( 'mollie_mandate_id' );
341
342
				if ( ! empty( $mandate_id ) ) {
343
					$request->set_mandate_id( $mandate_id );
344
				}
345
346
				if ( ! $subscription->is_first_payment( $payment ) ) {
0 ignored issues
show
The method is_first_payment() 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

346
				if ( ! $subscription->/** @scrutinizer ignore-call */ is_first_payment( $payment ) ) {

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...
347
					$request->set_method( null );
348
					$request->set_sequence_type( 'recurring' );
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( '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
		$attempt = (int) $payment->get_meta( 'mollie_create_payment_attempt' );
446
		$attempt = empty( $attempt ) ? 1 : $attempt + 1;
447
448
		$payment->set_meta( 'mollie_create_payment_attempt', $attempt );
449
450
		try {
451
			$result = $this->client->create_payment( $request );
452
453
			$payment->delete_meta( 'mollie_create_payment_attempt' );
0 ignored issues
show
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

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

472
			/** @scrutinizer ignore-call */ 
473
   \as_schedule_single_action(
Loading history...
473
				\time() + $this->get_retry_seconds( $attempt ),
474
				'pronamic_pay_mollie_payment_start',
475
				array(
476
					'payment_id' => $payment->get_id(),
477
				),
478
				'pronamic-pay-mollie'
479
			);
480
481
			// Add note.
482
			$payment->add_note(
483
				\sprintf(
484
					'%s - %s - %s',
485
					$error->get_status(),
486
					$error->get_title(),
487
					$error->get_detail()
488
				) 
489
			);
490
491
			$payment->save();
492
493
			return;
494
		}
495
496
		// phpcs:disable WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase -- Mollie JSON object.
497
498
		// Set transaction ID.
499
		if ( isset( $result->id ) ) {
500
			$payment->set_transaction_id( $result->id );
501
		}
502
503
		// Set payment method.
504
		if ( isset( $result->method ) ) {
505
			$payment_method = Methods::transform_gateway_method( $result->method );
506
507
			if ( null !== $payment_method ) {
508
				$payment->set_payment_method( $payment_method );
0 ignored issues
show
The method set_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

508
				$payment->/** @scrutinizer ignore-call */ 
509
              set_payment_method( $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...
509
510
				// Update subscription payment method.
511
				foreach ( $payment->get_subscriptions() as $subscription ) {
512
					$subscription->set_payment_method( $payment->get_payment_method() );
0 ignored issues
show
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

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

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