Test Failed
Push — develop ( ef35bf...4e8f2d )
by Remco
05:06
created

Gateway::get_supported_payment_methods()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 18
Code Lines 17

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 1

Importance

Changes 2
Bugs 0 Features 0
Metric Value
cc 1
eloc 17
c 2
b 0
f 0
nc 1
nop 0
dl 0
loc 18
ccs 2
cts 2
cp 1
crap 1
rs 9.7
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 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
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

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
Bug introduced by
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
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

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
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

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

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

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