Failed Conditions
Push — develop ( 47bed1...26b9bd )
by Remco
05:15
created

Gateway::get_customer_ids_for_user()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 12
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 6
CRAP Score 1

Importance

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

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

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

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

// Bar.php
namespace OtherDir;

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

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

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

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

// Bar.php
namespace OtherDir;

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

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

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

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

815
		/** @scrutinizer ignore-call */ 
816
  $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...
816
		$new_method = ( null === $payment_method && \property_exists( $mandate, 'method' ) ? Methods::transform_gateway_method( $mandate->method ) : $payment_method );
817
818
		// `Direct Debit` is not a recurring method, use `Direct Debit (mandate via ...)` instead.
819
		if ( PaymentMethods::DIRECT_DEBIT === $new_method ) {
820
			$new_method = PaymentMethods::DIRECT_DEBIT_IDEAL;
821
822
			// Use `Direct Debit (mandate via Bancontact)` if consumer account starts with `BE`.
823
			if ( \property_exists( $mandate, 'details' ) && 'BE' === \substr( $mandate->details->consumerAccount, 0, 2 ) ) {
824
				$new_method = PaymentMethods::DIRECT_DEBIT_BANCONTACT;
825
			}
826
		}
827
828
		if ( ! empty( $old_method ) && $old_method !== $new_method ) {
829
			$subscription->set_payment_method( $new_method );
0 ignored issues
show
Bug introduced by
The method set_payment_method() does not exist on Pronamic\WordPress\Pay\Subscriptions\Subscription. ( Ignorable by Annotation )

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

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