Test Failed
Push — develop ( 90cd3b...e7eb71 )
by Reüel
05:09
created

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

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

835
		/** @scrutinizer ignore-call */ 
836
  $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...
836
		$new_method = ( null === $payment_method && \property_exists( $mandate, 'method' ) ? Methods::transform_gateway_method( $mandate->method ) : $payment_method );
837
838
		// `Direct Debit` is not a recurring method, use `Direct Debit (mandate via ...)` instead.
839
		if ( PaymentMethods::DIRECT_DEBIT === $new_method ) {
840
			$new_method = PaymentMethods::DIRECT_DEBIT_IDEAL;
841
842
			// Use `Direct Debit (mandate via Bancontact)` if consumer account starts with `BE`.
843
			if ( \property_exists( $mandate, 'details' ) && 'BE' === \substr( $mandate->details->consumerAccount, 0, 2 ) ) {
844
				$new_method = PaymentMethods::DIRECT_DEBIT_BANCONTACT;
845
			}
846
		}
847
848
		if ( ! empty( $old_method ) && $old_method !== $new_method ) {
849
			$subscription->set_payment_method( $new_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

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