Test Failed
Push — develop ( ebfe7f...ef35bf )
by Remco
05:09
created

src/Gateway.php (7 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_method( null );
344
					$request->set_sequence_type( 'recurring' );
345
					$request->set_mandate_id( $mandate_id );
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( '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
		$attempt = (int) $payment->get_meta( 'mollie_create_payment_attempt' );
443
		$attempt = empty( $attempt ) ? 1 : $attempt + 1;
444
445
		$payment->set_meta( 'mollie_create_payment_attempt', $attempt );
446
447
		try {
448
			$result = $this->client->create_payment( $request );
449
450
			$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

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

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

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

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