Test Failed
Push — develop ( 2de508...9e96ac )
by Reüel
04:46
created

src/Gateway.php (6 issues)

1
<?php
2
/**
3
 * Mollie gateway.
4
 *
5
 * @author    Pronamic <[email protected]>
6
 * @copyright 2005-2021 Pronamic
7
 * @license   GPL-3.0-or-later
8
 * @package   Pronamic\WordPress\Pay
9
 */
10
11
namespace Pronamic\WordPress\Pay\Gateways\Mollie;
12
13
use Pronamic\WordPress\DateTime\DateTime;
14
use Pronamic\WordPress\Money\Money;
15
use Pronamic\WordPress\Pay\Banks\BankAccountDetails;
16
use Pronamic\WordPress\Pay\Banks\BankTransferDetails;
17
use Pronamic\WordPress\Pay\Core\Gateway as Core_Gateway;
18
use Pronamic\WordPress\Pay\Core\PaymentMethods;
19
use Pronamic\WordPress\Pay\Payments\FailureReason;
20
use Pronamic\WordPress\Pay\Payments\Payment;
0 ignored issues
show
This use statement conflicts with another class in this namespace, Pronamic\WordPress\Pay\Gateways\Mollie\Payment. Consider defining an alias.

Let?s assume that you have a directory layout like this:

.
|-- OtherDir
|   |-- Bar.php
|   `-- Foo.php
`-- SomeDir
    `-- Foo.php

and let?s assume the following content of Bar.php:

// Bar.php
namespace OtherDir;

use SomeDir\Foo; // This now conflicts the class OtherDir\Foo

If both files OtherDir/Foo.php and SomeDir/Foo.php are loaded in the same runtime, you will see a PHP error such as the following:

PHP Fatal error:  Cannot use SomeDir\Foo as Foo because the name is already in use in OtherDir/Foo.php

However, as OtherDir/Foo.php does not necessarily have to be loaded and the error is only triggered if it is loaded before OtherDir/Bar.php, this problem might go unnoticed for a while. In order to prevent this error from surfacing, you must import the namespace with a different alias:

// Bar.php
namespace OtherDir;

use SomeDir\Foo as SomeDirFoo; // There is no conflict anymore.
Loading history...
21
use Pronamic\WordPress\Pay\Payments\PaymentStatus;
22
use Pronamic\WordPress\Pay\Subscriptions\Subscription;
23
use Pronamic\WordPress\Pay\Subscriptions\SubscriptionStatus;
24
25
/**
26
 * Title: Mollie
27
 * Description:
28
 * Copyright: 2005-2021 Pronamic
29
 * Company: Pronamic
30
 *
31
 * @author  Remco Tolsma
32
 * @version 2.1.4
33
 * @since   1.1.0
34
 */
35
class Gateway extends Core_Gateway {
36
	/**
37
	 * Client.
38
	 *
39
	 * @var Client
40
	 */
41
	protected $client;
42
43
	/**
44
	 * Config
45
	 *
46
	 * @var Config
47
	 */
48
	protected $config;
49
50
	/**
51
	 * Profile data store.
52
	 *
53
	 * @var ProfileDataStore
54
	 */
55
	private $profile_data_store;
56
57
	/**
58
	 * Customer data store.
59
	 *
60
	 * @var CustomerDataStore
61
	 */
62
	private $customer_data_store;
63
64
	/**
65
	 * Constructs and initializes an Mollie gateway
66
	 *
67
	 * @param Config $config Config.
68
	 */
69 39
	public function __construct( Config $config ) {
70 39
		parent::__construct( $config );
71
72 39
		$this->set_method( self::METHOD_HTTP_REDIRECT );
73
74
		// Supported features.
75 39
		$this->supports = array(
76
			'payment_status_request',
77
			'recurring_direct_debit',
78
			'recurring_credit_card',
79
			'recurring',
80
			'refunds',
81
			'webhook',
82
			'webhook_log',
83
			'webhook_no_config',
84
		);
85
86
		// Client.
87 39
		$this->client = new Client( (string) $config->api_key );
88
89
		// Data Stores.
90 39
		$this->profile_data_store  = new ProfileDataStore();
91 39
		$this->customer_data_store = new CustomerDataStore();
92
93
		// Actions.
94 39
		add_action( 'pronamic_payment_status_update', array( $this, 'copy_customer_id_to_wp_user' ), 99, 1 );
95 39
	}
96
97
	/**
98
	 * Get issuers
99
	 *
100
	 * @see Core_Gateway::get_issuers()
101
	 * @return array<int, array<string, array<string>>>
102
	 */
103 3
	public function get_issuers() {
104 3
		$groups = array();
105
106
		try {
107 3
			$result = $this->client->get_issuers();
108
109
			$groups[] = array(
110
				'options' => $result,
111
			);
112 3
		} catch ( Error $e ) {
113
			// Catch Mollie error.
114 3
			$error = new \WP_Error(
115 3
				'mollie_error',
116 3
				sprintf( '%1$s (%2$s) - %3$s', $e->get_title(), $e->getCode(), $e->get_detail() )
117
			);
118
119 3
			$this->set_error( $error );
120
		} catch ( \Exception $e ) {
121
			// Catch exceptions.
122
			$error = new \WP_Error( 'mollie_error', $e->getMessage() );
123
124
			$this->set_error( $error );
125
		}
126
127 3
		return $groups;
128
	}
129
130
	/**
131
	 * Get available payment methods.
132
	 *
133
	 * @see Core_Gateway::get_available_payment_methods()
134
	 * @return array<int, string>
135
	 */
136 2
	public function get_available_payment_methods() {
137 2
		$payment_methods = array();
138
139
		// Set sequence types to get payment methods for.
140 2
		$sequence_types = array( Sequence::ONE_OFF, Sequence::RECURRING, Sequence::FIRST );
141
142 2
		$results = array();
143
144 2
		foreach ( $sequence_types as $sequence_type ) {
145
			// Get active payment methods for Mollie account.
146
			try {
147 2
				$result = $this->client->get_payment_methods( $sequence_type );
148 2
			} catch ( Error $e ) {
149
				// Catch Mollie error.
150
				$error = new \WP_Error(
151
					'mollie_error',
152
					sprintf( '%1$s (%2$s) - %3$s', $e->get_title(), $e->getCode(), $e->get_detail() )
153
				);
154
155
				$this->set_error( $error );
156
157
				break;
158 2
			} catch ( \Exception $e ) {
159
				// Catch exceptions.
160 2
				$error = new \WP_Error( 'mollie_error', $e->getMessage() );
161
162 2
				$this->set_error( $error );
163
164 2
				break;
165
			}
166
167 2
			if ( Sequence::FIRST === $sequence_type ) {
168
				foreach ( $result as $method => $title ) {
169
					unset( $result[ $method ] );
170
171
					// Get WordPress payment method for direct debit method.
172
					$method         = Methods::transform_gateway_method( $method );
173
					$payment_method = array_search( $method, PaymentMethods::get_recurring_methods(), true );
174
175
					if ( $payment_method ) {
176
						$results[ $payment_method ] = $title;
177
					}
178
				}
179
			}
180
181 2
			if ( is_array( $result ) ) {
182 2
				$results = array_merge( $results, $result );
183
			}
184
		}
185
186
		// Transform to WordPress payment methods.
187 2
		foreach ( $results as $method => $title ) {
188 2
			$method = (string) $method;
189
190 2
			$payment_method = Methods::transform_gateway_method( $method );
191
192 2
			if ( PaymentMethods::is_recurring_method( $method ) ) {
193
				$payment_method = $method;
194
			}
195
196 2
			if ( null !== $payment_method ) {
197 2
				$payment_methods[] = (string) $payment_method;
198
			}
199
		}
200
201 2
		$payment_methods = array_unique( $payment_methods );
202
203 2
		return $payment_methods;
204
	}
205
206
	/**
207
	 * Get supported payment methods
208
	 *
209
	 * @see Core_Gateway::get_supported_payment_methods()
210
	 * @return array<string>
211
	 */
212 2
	public function get_supported_payment_methods() {
213
		return array(
214 2
			PaymentMethods::APPLE_PAY,
215
			PaymentMethods::BANCONTACT,
216
			PaymentMethods::BANK_TRANSFER,
217
			PaymentMethods::BELFIUS,
218
			PaymentMethods::CREDIT_CARD,
219
			PaymentMethods::DIRECT_DEBIT,
220
			PaymentMethods::DIRECT_DEBIT_BANCONTACT,
221
			PaymentMethods::DIRECT_DEBIT_IDEAL,
222
			PaymentMethods::DIRECT_DEBIT_SOFORT,
223
			PaymentMethods::EPS,
224
			PaymentMethods::GIROPAY,
225
			PaymentMethods::IDEAL,
226
			PaymentMethods::KBC,
227
			PaymentMethods::PAYPAL,
228
			PaymentMethods::PRZELEWY24,
229
			PaymentMethods::SOFORT,
230
		);
231
	}
232
233
	/**
234
	 * Get webhook URL for Mollie.
235
	 *
236
	 * @return string|null
237
	 */
238 4
	public function get_webhook_url() {
239 4
		$url = \rest_url( Integration::REST_ROUTE_NAMESPACE . '/webhook' );
240
241 4
		$host = wp_parse_url( $url, PHP_URL_HOST );
242
243 4
		if ( is_array( $host ) ) {
244
			// Parsing failure.
245
			$host = '';
246
		}
247
248 4
		if ( 'localhost' === $host ) {
249
			// Mollie doesn't allow localhost.
250 1
			return null;
251 3
		} elseif ( '.dev' === substr( $host, -4 ) ) {
252
			// Mollie doesn't allow the .dev TLD.
253 1
			return null;
254 2
		} elseif ( '.local' === substr( $host, -6 ) ) {
255
			// Mollie doesn't allow the .local TLD.
256 1
			return null;
257 1
		} elseif ( '.test' === substr( $host, -5 ) ) {
258
			// Mollie doesn't allow the .test TLD.
259
			return null;
260
		}
261
262 1
		return $url;
263
	}
264
265
	/**
266
	 * Start
267
	 *
268
	 * @see Core_Gateway::start()
269
	 * @param Payment $payment Payment.
270
	 * @return void
271
	 * @throws \Exception Throws exception on error creating Mollie customer for payment.
272
	 */
273
	public function start( Payment $payment ) {
274
		$description = (string) $payment->get_description();
275
276
		/**
277
		 * Filters the Mollie payment description.
278
		 * 
279
		 * The maximum length of the description field differs per payment
280
		 * method, with the absolute maximum being 255 characters.
281
		 *
282
		 * @link https://docs.mollie.com/reference/v2/payments-api/create-payment#parameters
283
		 * @since 3.0.1
284
		 * @param string  $description Description.
285
		 * @param Payment $payment     Payment.
286
		 */
287
		$description = \apply_filters( 'pronamic_pay_mollie_payment_description', $description, $payment );
288
289
		$request = new PaymentRequest(
290
			AmountTransformer::transform( $payment->get_total_amount() ),
291
			$description
292
		);
293
294
		$request->redirect_url = $payment->get_return_url();
295
		$request->webhook_url  = $this->get_webhook_url();
296
297
		$schedule_retry_on_error = false;
298
299
		// Locale.
300
		$customer = $payment->get_customer();
301
302
		if ( null !== $customer ) {
303
			$request->locale = LocaleHelper::transform( $customer->get_locale() );
304
		}
305
306
		// Customer ID.
307
		$customer_id = $this->get_customer_id_for_payment( $payment );
308
309
		if ( null === $customer_id ) {
310
			$customer_id = $this->create_customer_for_payment( $payment );
311
		}
312
313
		if ( null !== $customer_id ) {
314
			$request->customer_id = $customer_id;
315
		}
316
317
		/**
318
		 * Payment method.
319
		 *
320
		 * Leap of faith if the WordPress payment method could not transform to a Mollie method?
321
		 */
322
		$payment_method = $payment->get_payment_method();
0 ignored issues
show
The method get_payment_method() does not exist on Pronamic\WordPress\Pay\Payments\Payment. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

322
		/** @scrutinizer ignore-call */ 
323
  $payment_method = $payment->get_payment_method();

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
323
324
		$request->set_method( Methods::transform( $payment_method, $payment_method ) );
325
326
		/**
327
		 * Sequence type.
328
		 *
329
		 * Recurring payments are created through the Payments API by providing a `sequenceType`.
330
		 */
331
		$subscriptions = $payment->get_subscriptions();
332
333
		if ( \count( $subscriptions ) > 0 ) {
334
			$first_method = PaymentMethods::get_first_payment_method( $payment_method );
335
336
			$request->set_method( Methods::transform( $first_method, $first_method ) );
337
338
			$request->set_sequence_type( 'first' );
339
340
			foreach ( $subscriptions as $subscription ) {
341
				$mandate_id = $subscription->get_meta( 'mollie_mandate_id' );
342
343
				if ( ! empty( $mandate_id ) ) {
344
					$request->set_method( null );
345
					$request->set_sequence_type( 'recurring' );
346
					$request->set_mandate_id( $mandate_id );
347
348
					$schedule_retry_on_error = true;
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( Sequence::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
		try {
446
			$result = $this->client->create_payment( $request );
447
		} catch ( \Exception $e ) {
448
			if ( $schedule_retry_on_error ) {
449
				\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

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

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

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