Failed Conditions
Push — develop ( f7eda8...29a091 )
by Remco
05:04 queued 11s
created

src/Gateway.php (5 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
		// Locale.
298
		$customer = $payment->get_customer();
299
300
		if ( null !== $customer ) {
301
			$request->locale = LocaleHelper::transform( $customer->get_locale() );
302
		}
303
304
		// Customer ID.
305
		$customer_id = $this->get_customer_id_for_payment( $payment );
306
307
		if ( null === $customer_id ) {
308
			$customer_id = $this->create_customer_for_payment( $payment );
309
		}
310
311
		if ( null !== $customer_id ) {
312
			$request->customer_id = $customer_id;
313
		}
314
315
		// Payment method.
316
		$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

316
		/** @scrutinizer ignore-call */ 
317
  $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...
317
318
		// Recurring payment method.
319
		$subscription = $payment->get_subscription();
320
321
		$is_recurring_method = ( $subscription && PaymentMethods::is_recurring_method( (string) $payment_method ) );
322
323
		// Consumer bank details.
324
		$consumer_bank_details = $payment->get_consumer_bank_details();
325
326
		if ( PaymentMethods::DIRECT_DEBIT === $payment_method && null !== $consumer_bank_details ) {
327
			$consumer_name = $consumer_bank_details->get_name();
328
			$consumer_iban = $consumer_bank_details->get_iban();
329
330
			$request->consumer_name    = $consumer_name;
331
			$request->consumer_account = $consumer_iban;
332
333
			// Check if one-off SEPA Direct Debit can be used, otherwise short circuit payment.
334
			if ( null !== $customer_id ) {
335
				// Find or create mandate.
336
				$mandate_id = $this->client->has_valid_mandate( $customer_id, PaymentMethods::DIRECT_DEBIT, $consumer_iban );
337
338
				if ( false === $mandate_id ) {
339
					$mandate = $this->client->create_mandate( $customer_id, $consumer_bank_details );
340
341
					if ( ! \property_exists( $mandate, 'id' ) ) {
342
						throw new \Exception( 'Missing mandate ID.' );
343
					}
344
345
					$mandate_id = $mandate->id;
346
				}
347
348
				// Charge immediately on-demand.
349
				$request->set_sequence_type( Sequence::RECURRING );
350
				$request->set_mandate_id( (string) $mandate_id );
351
352
				$is_recurring_method = true;
353
354
				$payment->recurring = true;
355
			}
356
		}
357
358
		if ( false === $is_recurring_method && null !== $payment_method ) {
359
			// Always use 'direct debit mandate via iDEAL/Bancontact/Sofort' payment methods as recurring method.
360
			$is_recurring_method = PaymentMethods::is_direct_debit_method( $payment_method );
361
362
			// Check for non-recurring methods for subscription payments.
363
			if ( false === $is_recurring_method && null !== $payment->get_periods() ) {
364
				$direct_debit_methods = PaymentMethods::get_direct_debit_methods();
365
366
				$is_recurring_method = \in_array( $payment_method, $direct_debit_methods, true );
367
			}
368
		}
369
370
		if ( $is_recurring_method ) {
371
			$request->sequence_type = $payment->get_recurring() ? Sequence::RECURRING : Sequence::FIRST;
372
373
			if ( Sequence::FIRST === $request->sequence_type ) {
374
				$payment_method = PaymentMethods::get_first_payment_method( $payment_method );
375
			}
376
377
			if ( Sequence::RECURRING === $request->sequence_type ) {
378
				// Use mandate from subscription.
379
				if ( $subscription && empty( $request->mandate_id ) ) {
380
					$subscription_mandate_id = $subscription->get_meta( 'mollie_mandate_id' );
381
382
					if ( false !== $subscription_mandate_id ) {
383
						$request->set_mandate_id( $subscription_mandate_id );
384
					}
385
				}
386
387
				// Use credit card for recurring Apple Pay payments.
388
				if ( PaymentMethods::APPLE_PAY === $payment_method ) {
389
					$payment_method = PaymentMethods::CREDIT_CARD;
390
				}
391
392
				$direct_debit_methods = PaymentMethods::get_direct_debit_methods();
393
394
				$recurring_method = \array_search( $payment_method, $direct_debit_methods, true );
395
396
				if ( \is_string( $recurring_method ) ) {
397
					$payment_method = $recurring_method;
398
				}
399
400
				$payment->set_action_url( $payment->get_return_url() );
401
			}
402
		}
403
404
		/**
405
		 * Sequence type.
406
		 *
407
		 * Recurring payments are created through the Payments API by providing a `sequenceType`.
408
		 */
409
		$subscriptions = $payment->get_subscriptions();
410
411
		if ( \count( $subscriptions ) > 0 ) {
412
			$request->sequence_type = 'first';
413
414
			foreach ( $subscriptions as $subscription ) {
415
				$mandate_id = $subscription->get_meta( 'mollie_mandate_id' );
416
417
				if ( ! empty( $mandate_id ) ) {
418
					$request->sequence_type = 'recurring';
419
					$request->mandate_id    = $mandate_id;
420
				}
421
			}
422
		}
423
424
		/**
425
		 * Payment method.
426
		 *
427
		 * Leap of faith if the WordPress payment method could not transform to a Mollie method?
428
		 */
429
		$request->method = Methods::transform( $payment_method, $payment_method );
430
431
		/**
432
		 * Metadata.
433
		 *
434
		 * Provide any data you like, for example a string or a JSON object.
435
		 * We will save the data alongside the payment. Whenever you fetch
436
		 * the payment with our API, we’ll also include the metadata. You
437
		 * can use up to approximately 1kB.
438
		 *
439
		 * @link https://docs.mollie.com/reference/v2/payments-api/create-payment
440
		 * @link https://en.wikipedia.org/wiki/Metadata
441
		 */
442
		$metadata = null;
443
444
		/**
445
		 * Filters the Mollie metadata.
446
		 *
447
		 * @since 2.2.0
448
		 *
449
		 * @param mixed   $metadata Metadata.
450
		 * @param Payment $payment  Payment.
451
		 */
452
		$metadata = \apply_filters( 'pronamic_pay_mollie_payment_metadata', $metadata, $payment );
453
454
		$request->set_metadata( $metadata );
455
456
		// Issuer.
457
		if ( Methods::IDEAL === $request->method ) {
458
			$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...
459
		}
460
461
		// Billing email.
462
		$billing_email = $payment->get_email();
463
464
		/**
465
		 * Filters the Mollie payment billing email used for bank transfer payment instructions.
466
		 *
467
		 * @since 2.2.0
468
		 *
469
		 * @param string|null $billing_email Billing email.
470
		 * @param Payment     $payment       Payment.
471
		 */
472
		$billing_email = \apply_filters( 'pronamic_pay_mollie_payment_billing_email', $billing_email, $payment );
473
474
		$request->set_billing_email( $billing_email );
475
476
		// Due date.
477
		if ( ! empty( $this->config->due_date_days ) ) {
478
			try {
479
				$due_date = new DateTime( sprintf( '+%s days', $this->config->due_date_days ) );
480
			} catch ( \Exception $e ) {
481
				$due_date = null;
482
			}
483
484
			$request->set_due_date( $due_date );
485
		}
486
487
		// Create payment.
488
		$result = $this->client->create_payment( $request );
489
490
		// phpcs:disable WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase -- Mollie JSON object.
491
492
		// Set transaction ID.
493
		if ( isset( $result->id ) ) {
494
			$payment->set_transaction_id( $result->id );
495
		}
496
497
		// Set expiry date.
498
		if ( isset( $result->expiresAt ) ) {
499
			try {
500
				$expires_at = new DateTime( $result->expiresAt );
501
			} catch ( \Exception $e ) {
502
				$expires_at = null;
503
			}
504
505
			$payment->set_expiry_date( $expires_at );
506
		}
507
508
		// Set status.
509
		if ( isset( $result->status ) ) {
510
			$payment->set_status( Statuses::transform( $result->status ) );
511
		}
512
513
		// Set bank transfer recipient details.
514
		if ( isset( $result->details ) ) {
515
			$bank_transfer_recipient_details = $payment->get_bank_transfer_recipient_details();
516
517
			if ( null === $bank_transfer_recipient_details ) {
518
				$bank_transfer_recipient_details = new BankTransferDetails();
519
520
				$payment->set_bank_transfer_recipient_details( $bank_transfer_recipient_details );
521
			}
522
523
			$bank_details = $bank_transfer_recipient_details->get_bank_account();
524
525
			if ( null === $bank_details ) {
526
				$bank_details = new BankAccountDetails();
527
528
				$bank_transfer_recipient_details->set_bank_account( $bank_details );
529
			}
530
531
			$details = $result->details;
532
533
			if ( isset( $details->bankName ) ) {
534
				/**
535
				 * Set `bankName` as bank details name, as result "Stichting Mollie Payments"
536
				 * is not the name of a bank, but the account holder name.
537
				 */
538
				$bank_details->set_name( $details->bankName );
539
			}
540
541
			if ( isset( $details->bankAccount ) ) {
542
				$bank_details->set_iban( $details->bankAccount );
543
			}
544
545
			if ( isset( $details->bankBic ) ) {
546
				$bank_details->set_bic( $details->bankBic );
547
			}
548
549
			if ( isset( $details->transferReference ) ) {
550
				$bank_transfer_recipient_details->set_reference( $details->transferReference );
551
			}
552
		}
553
554
		// Handle links.
555
		if ( isset( $result->_links ) ) {
556
			$links = $result->_links;
557
558
			// Action URL.
559
			if ( isset( $links->checkout->href ) ) {
560
				$payment->set_action_url( $links->checkout->href );
561
			}
562
563
			// Change payment state URL.
564
			if ( isset( $links->changePaymentState->href ) ) {
565
				$payment->set_meta( 'mollie_change_payment_state_url', $links->changePaymentState->href );
566
			}
567
		}
568
569
		// phpcs:enable WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase -- Mollie JSON object.
570
	}
571
572
	/**
573
	 * Update status of the specified payment
574
	 *
575
	 * @param Payment $payment Payment.
576
	 * @return void
577
	 */
578
	public function update_status( Payment $payment ) {
579
		$transaction_id = $payment->get_transaction_id();
580
581
		if ( null === $transaction_id ) {
582
			return;
583
		}
584
585
		$mollie_payment = $this->client->get_payment( $transaction_id );
586
587
		$payment->set_status( Statuses::transform( $mollie_payment->get_status() ) );
588
589
		/**
590
		 * Mollie profile.
591
		 */
592
		$mollie_profile = new Profile();
593
594
		$mollie_profile->set_id( $mollie_payment->get_profile_id() );
595
596
		$profile_internal_id = $this->profile_data_store->get_or_insert_profile( $mollie_profile );
597
598
		/**
599
		 * If the Mollie payment contains a customer ID we will try to connect
600
		 * this Mollie customer ID the WordPress user and subscription.
601
		 * This can be useful in case when a WordPress user is created after
602
		 * a successful payment.
603
		 *
604
		 * @link https://www.gravityforms.com/add-ons/user-registration/
605
		 */
606
		$mollie_customer_id = $mollie_payment->get_customer_id();
607
608
		if ( null !== $mollie_customer_id ) {
609
			$mollie_customer = new Customer( $mollie_customer_id );
610
611
			$customer_internal_id = $this->customer_data_store->get_or_insert_customer(
612
				$mollie_customer,
613
				array(
614
					'profile_id' => $profile_internal_id,
615
				),
616
				array(
617
					'profile_id' => '%s',
618
				)
619
			);
620
621
			// Meta.
622
			$customer_id = $payment->get_meta( 'mollie_customer_id' );
623
624
			if ( empty( $customer_id ) ) {
625
				$payment->set_meta( 'mollie_customer_id', $mollie_customer->get_id() );
626
			}
627
628
			// Customer.
629
			$customer = $payment->get_customer();
630
631
			if ( null !== $customer ) {
632
				// Connect to user.
633
				$user_id = $customer->get_user_id();
634
635
				if ( null !== $user_id ) {
636
					$user = \get_user_by( 'id', $user_id );
637
638
					if ( false !== $user ) {
639
						$this->customer_data_store->connect_mollie_customer_to_wp_user( $mollie_customer, $user );
640
					}
641
				}
642
			}
643
644
			// Subscription.
645
			$subscription = $payment->get_subscription();
646
647
			if ( null !== $subscription ) {
648
				$customer_id = $subscription->get_meta( 'mollie_customer_id' );
649
650
				if ( empty( $customer_id ) ) {
651
					$subscription->set_meta( 'mollie_customer_id', $mollie_customer->get_id() );
652
				}
653
654
				// Update mandate in subscription meta.
655
				$mollie_mandate_id = $mollie_payment->get_mandate_id();
656
657
				if ( null !== $mollie_mandate_id ) {
658
					$mandate_id = $subscription->get_meta( 'mollie_mandate_id' );
659
660
					// Only update if no mandate has been set yet or if payment succeeded.
661
					if ( empty( $mandate_id ) || PaymentStatus::SUCCESS === $payment->get_status() ) {
662
						$this->update_subscription_mandate( $subscription, $mollie_mandate_id, $payment->get_payment_method() );
663
					}
664
				}
665
			}
666
		}
667
668
		// phpcs:disable WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase -- Mollie JSON object.
669
		$mollie_payment_details = $mollie_payment->get_details();
670
671
		if ( null !== $mollie_payment_details ) {
672
			$consumer_bank_details = $payment->get_consumer_bank_details();
673
674
			if ( null === $consumer_bank_details ) {
675
				$consumer_bank_details = new BankAccountDetails();
676
677
				$payment->set_consumer_bank_details( $consumer_bank_details );
678
			}
679
680
			if ( isset( $mollie_payment_details->consumerName ) ) {
681
				$consumer_bank_details->set_name( $mollie_payment_details->consumerName );
682
			}
683
684
			if ( isset( $mollie_payment_details->cardHolder ) ) {
685
				$consumer_bank_details->set_name( $mollie_payment_details->cardHolder );
686
			}
687
688
			if ( isset( $mollie_payment_details->cardNumber ) ) {
689
				// The last four digits of the card number.
690
				$consumer_bank_details->set_account_number( $mollie_payment_details->cardNumber );
691
			}
692
693
			if ( isset( $mollie_payment_details->cardCountryCode ) ) {
694
				// The ISO 3166-1 alpha-2 country code of the country the card was issued in.
695
				$consumer_bank_details->set_country( $mollie_payment_details->cardCountryCode );
696
			}
697
698
			if ( isset( $mollie_payment_details->consumerAccount ) ) {
699
				switch ( $mollie_payment->get_method() ) {
700
					case Methods::BELFIUS:
701
					case Methods::DIRECT_DEBIT:
702
					case Methods::IDEAL:
703
					case Methods::KBC:
704
					case Methods::SOFORT:
705
						$consumer_bank_details->set_iban( $mollie_payment_details->consumerAccount );
706
707
						break;
708
					case Methods::BANCONTACT:
709
					case Methods::BANKTRANSFER:
710
					case Methods::PAYPAL:
711
					default:
712
						$consumer_bank_details->set_account_number( $mollie_payment_details->consumerAccount );
713
714
						break;
715
				}
716
			}
717
718
			if ( isset( $mollie_payment_details->consumerBic ) ) {
719
				$consumer_bank_details->set_bic( $mollie_payment_details->consumerBic );
720
			}
721
722
			/*
723
			 * Failure reason.
724
			 */
725
			$failure_reason = $payment->get_failure_reason();
726
727
			if ( null === $failure_reason ) {
728
				$failure_reason = new FailureReason();
729
			}
730
731
			// SEPA Direct Debit.
732
			if ( isset( $mollie_payment_details->bankReasonCode ) ) {
733
				$failure_reason->set_code( $mollie_payment_details->bankReasonCode );
734
			}
735
736
			if ( isset( $mollie_payment_details->bankReason ) ) {
737
				$failure_reason->set_message( $mollie_payment_details->bankReason );
738
			}
739
740
			// Credit card.
741
			if ( isset( $mollie_payment_details->failureReason ) ) {
742
				$failure_reason->set_code( $mollie_payment_details->failureReason );
743
			}
744
745
			if ( isset( $mollie_payment_details->failureMessage ) ) {
746
				$failure_reason->set_message( $mollie_payment_details->failureMessage );
747
			}
748
749
			$failure_code    = $failure_reason->get_code();
750
			$failure_message = $failure_reason->get_message();
751
752
			if ( ! empty( $failure_code ) || ! empty( $failure_message ) ) {
753
				$payment->set_failure_reason( $failure_reason );
754
			}
755
		}
756
757
		$links = $mollie_payment->get_links();
758
759
		// Change payment state URL.
760
		if ( \property_exists( $links, 'changePaymentState' ) ) {
761
			$payment->set_meta( 'mollie_change_payment_state_url', $links->changePaymentState->href );
762
		}
763
764
		// phpcs:enable WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase -- Mollie JSON object.
765
766
		if ( $mollie_payment->has_chargebacks() ) {
767
			$mollie_chargebacks = $this->client->get_payment_chargebacks(
768
				$mollie_payment->get_id(),
769
				array( 'limit' => 1 )
770
			);
771
772
			$mollie_chargeback = \reset( $mollie_chargebacks );
773
774
			if ( false !== $mollie_chargeback ) {
775
				$subscriptions = array_filter(
776
					$payment->get_subscriptions(),
777
					function( $subscription ) {
778
						return SubscriptionStatus::ACTIVE === $subscription->get_status();
779
					}
780
				);
781
782
				foreach ( $subscriptions as $subscription ) {
783
					if ( $mollie_chargeback->get_created_at() > $subscription->get_activated_at() ) {
784
						$subscription->set_status( SubscriptionStatus::ON_HOLD );
785
786
						$subscription->add_note(
787
							\sprintf(
788
								/* translators: 1: Mollie chargeback ID, 2: Mollie payment ID */
789
								\__( 'Subscription put on hold due to chargeback `%1$s` of payment `%2$s`.', 'pronamic_ideal' ),
790
								\esc_html( $mollie_chargeback->get_id() ),
791
								\esc_html( $mollie_payment->get_id() )
792
							)
793
						);
794
795
						$subscription->save();
796
					}
797
				}
798
			}
799
		}
800
801
		// Refunds.
802
		$amount_refunded = $mollie_payment->get_amount_refunded();
803
804
		if ( null !== $amount_refunded ) {
805
			$refunded_amount = new Money( $amount_refunded->get_value(), $amount_refunded->get_currency() );
806
807
			$payment->set_refunded_amount( $refunded_amount->get_value() > 0 ? $refunded_amount : null );
808
		}
809
	}
810
811
	/**
812
	 * Update subscription mandate.
813
	 *
814
	 * @param Subscription $subscription   Subscription.
815
	 * @param string       $mandate_id     Mollie mandate ID.
816
	 * @param string|null  $payment_method Payment method.
817
	 * @return void
818
	 * @throws \Exception Throws exception if subscription note could not be added.
819
	 */
820
	public function update_subscription_mandate( Subscription $subscription, $mandate_id, $payment_method = null ) {
821
		$customer_id = (string) $subscription->get_meta( 'mollie_customer_id' );
822
823
		$mandate = $this->client->get_mandate( $mandate_id, $customer_id );
824
825
		if ( ! \is_object( $mandate ) ) {
826
			return;
827
		}
828
829
		// Update mandate.
830
		$old_mandate_id = $subscription->get_meta( 'mollie_mandate_id' );
831
832
		$subscription->set_meta( 'mollie_mandate_id', $mandate_id );
833
834
		if ( ! empty( $old_mandate_id ) && $old_mandate_id !== $mandate_id ) {
835
			$note = \sprintf(
836
			/* translators: 1: old mandate ID, 2: new mandate ID */
837
				\__( 'Mandate for subscription changed from "%1$s" to "%2$s".', 'pronamic_ideal' ),
838
				\esc_html( $old_mandate_id ),
839
				\esc_html( $mandate_id )
840
			);
841
842
			$subscription->add_note( $note );
843
		}
844
845
		// Update payment method.
846
		$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

846
		/** @scrutinizer ignore-call */ 
847
  $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...
847
		$new_method = ( null === $payment_method && \property_exists( $mandate, 'method' ) ? Methods::transform_gateway_method( $mandate->method ) : $payment_method );
848
849
		// `Direct Debit` is not a recurring method, use `Direct Debit (mandate via ...)` instead.
850
		if ( PaymentMethods::DIRECT_DEBIT === $new_method ) {
851
			$new_method = PaymentMethods::DIRECT_DEBIT_IDEAL;
852
853
			// Use `Direct Debit (mandate via Bancontact)` if consumer account starts with `BE`.
854
			if ( \property_exists( $mandate, 'details' ) && 'BE' === \substr( $mandate->details->consumerAccount, 0, 2 ) ) {
855
				$new_method = PaymentMethods::DIRECT_DEBIT_BANCONTACT;
856
			}
857
		}
858
859
		if ( ! empty( $old_method ) && $old_method !== $new_method ) {
860
			$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

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