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

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

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