Test Failed
Push — develop ( 745105...19a60e )
by Reüel
04:36
created

src/Gateway.php (2 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\Subscriptions\Subscription;
22
use Pronamic\WordPress\Pay\Subscriptions\SubscriptionStatus;
23
24
/**
25
 * Title: Mollie
26
 * Description:
27
 * Copyright: 2005-2021 Pronamic
28
 * Company: Pronamic
29
 *
30
 * @author  Remco Tolsma
31
 * @version 2.1.4
32
 * @since   1.1.0
33
 */
34
class Gateway extends Core_Gateway {
35
	/**
36
	 * Client.
37
	 *
38
	 * @var Client
39
	 */
40
	protected $client;
41
42
	/**
43
	 * Config
44
	 *
45
	 * @var Config
46
	 */
47
	protected $config;
48
49
	/**
50
	 * Profile data store.
51
	 *
52
	 * @var ProfileDataStore
53
	 */
54
	private $profile_data_store;
55
56
	/**
57
	 * Customer data store.
58
	 *
59
	 * @var CustomerDataStore
60
	 */
61
	private $customer_data_store;
62
63
	/**
64
	 * Constructs and initializes an Mollie gateway
65
	 *
66
	 * @param Config $config Config.
67
	 */
68
	public function __construct( Config $config ) {
69 39
		parent::__construct( $config );
70 39
71
		$this->set_method( self::METHOD_HTTP_REDIRECT );
72 39
73
		// Supported features.
74
		$this->supports = array(
75 39
			'payment_status_request',
76
			'recurring_direct_debit',
77
			'recurring_credit_card',
78
			'recurring',
79
			'refunds',
80
			'webhook',
81
			'webhook_log',
82
			'webhook_no_config',
83
		);
84
85
		// Client.
86
		$this->client = new Client( (string) $config->api_key );
87 39
88
		// Data Stores.
89
		$this->profile_data_store  = new ProfileDataStore();
90 39
		$this->customer_data_store = new CustomerDataStore();
91 39
92
		// Actions.
93
		add_action( 'pronamic_payment_status_update', array( $this, 'copy_customer_id_to_wp_user' ), 99, 1 );
94 39
	}
95 39
96
	/**
97
	 * Get issuers
98
	 *
99
	 * @see Core_Gateway::get_issuers()
100
	 * @return array<int, array<string, array<string>>>
101
	 */
102
	public function get_issuers() {
103 3
		$groups = array();
104 3
105
		try {
106
			$result = $this->client->get_issuers();
107 3
108
			$groups[] = array(
109
				'options' => $result,
110
			);
111
		} catch ( Error $e ) {
112 3
			// Catch Mollie error.
113
			$error = new \WP_Error(
114 3
				'mollie_error',
115 3
				sprintf( '%1$s (%2$s) - %3$s', $e->get_title(), $e->getCode(), $e->get_detail() )
116 3
			);
117
118
			$this->set_error( $error );
119 3
		} catch ( \Exception $e ) {
120
			// Catch exceptions.
121
			$error = new \WP_Error( 'mollie_error', $e->getMessage() );
122
123
			$this->set_error( $error );
124
		}
125
126
		return $groups;
127 3
	}
128
129
	/**
130
	 * Get available payment methods.
131
	 *
132
	 * @see Core_Gateway::get_available_payment_methods()
133
	 * @return array<int, string>
134
	 */
135
	public function get_available_payment_methods() {
136 2
		$payment_methods = array();
137 2
138
		// Set sequence types to get payment methods for.
139
		$sequence_types = array( Sequence::ONE_OFF, Sequence::RECURRING, Sequence::FIRST );
140 2
141
		$results = array();
142 2
143
		foreach ( $sequence_types as $sequence_type ) {
144 2
			// Get active payment methods for Mollie account.
145
			try {
146
				$result = $this->client->get_payment_methods( $sequence_type );
147 2
			} catch ( Error $e ) {
148 2
				// Catch Mollie error.
149
				$error = new \WP_Error(
150
					'mollie_error',
151
					sprintf( '%1$s (%2$s) - %3$s', $e->get_title(), $e->getCode(), $e->get_detail() )
152
				);
153
154
				$this->set_error( $error );
155
156
				break;
157
			} catch ( \Exception $e ) {
158 2
				// Catch exceptions.
159
				$error = new \WP_Error( 'mollie_error', $e->getMessage() );
160 2
161
				$this->set_error( $error );
162 2
163
				break;
164 2
			}
165
166
			if ( Sequence::FIRST === $sequence_type ) {
167 2
				foreach ( $result as $method => $title ) {
168
					unset( $result[ $method ] );
169
170
					// Get WordPress payment method for direct debit method.
171
					$method         = Methods::transform_gateway_method( $method );
172
					$payment_method = array_search( $method, PaymentMethods::get_recurring_methods(), true );
173
174
					if ( $payment_method ) {
175
						$results[ $payment_method ] = $title;
176
					}
177
				}
178
			}
179
180
			if ( is_array( $result ) ) {
181 2
				$results = array_merge( $results, $result );
182 2
			}
183
		}
184
185
		// Transform to WordPress payment methods.
186
		foreach ( $results as $method => $title ) {
187 2
			$method = (string) $method;
188 2
189
			$payment_method = Methods::transform_gateway_method( $method );
190 2
191
			if ( PaymentMethods::is_recurring_method( $method ) ) {
192 2
				$payment_method = $method;
193
			}
194
195
			if ( null !== $payment_method ) {
196 2
				$payment_methods[] = (string) $payment_method;
197 2
			}
198
		}
199
200
		$payment_methods = array_unique( $payment_methods );
201 2
202
		return $payment_methods;
203 2
	}
204
205
	/**
206
	 * Get supported payment methods
207
	 *
208
	 * @see Core_Gateway::get_supported_payment_methods()
209
	 * @return array<string>
210
	 */
211
	public function get_supported_payment_methods() {
212 2
		return array(
213
			PaymentMethods::APPLE_PAY,
214 2
			PaymentMethods::BANCONTACT,
215
			PaymentMethods::BANK_TRANSFER,
216
			PaymentMethods::BELFIUS,
217
			PaymentMethods::CREDIT_CARD,
218
			PaymentMethods::DIRECT_DEBIT,
219
			PaymentMethods::DIRECT_DEBIT_BANCONTACT,
220
			PaymentMethods::DIRECT_DEBIT_IDEAL,
221
			PaymentMethods::DIRECT_DEBIT_SOFORT,
222
			PaymentMethods::EPS,
223
			PaymentMethods::GIROPAY,
224
			PaymentMethods::IDEAL,
225
			PaymentMethods::KBC,
226
			PaymentMethods::PAYPAL,
227
			PaymentMethods::PRZELEWY24,
228
			PaymentMethods::SOFORT,
229
		);
230
	}
231
232
	/**
233
	 * Get webhook URL for Mollie.
234
	 *
235
	 * @param Payment $payment Payment.
236
	 * @return string|null
237
	 */
238 4
	public function get_webhook_url( Payment $payment ) {
239 4
		$url = \rest_url( Integration::REST_ROUTE_NAMESPACE . '/webhook/' . (string) $payment->get_id() );
240
241 4
		$host = wp_parse_url( $url, PHP_URL_HOST );
242
243 4
		if ( is_array( $host ) ) {
244
			// Parsing failure.
245
			$host = '';
246
		}
247
248 4
		if ( 'localhost' === $host ) {
249
			// Mollie doesn't allow localhost.
250 1
			return null;
251 3
		} elseif ( '.dev' === substr( $host, -4 ) ) {
252
			// Mollie doesn't allow the .dev TLD.
253 1
			return null;
254 2
		} elseif ( '.local' === substr( $host, -6 ) ) {
255
			// Mollie doesn't allow the .local TLD.
256 1
			return null;
257 1
		} elseif ( '.test' === substr( $host, -5 ) ) {
258
			// Mollie doesn't allow the .test TLD.
259
			return null;
260
		}
261
262 1
		return $url;
263
	}
264
265
	/**
266
	 * Start
267
	 *
268
	 * @see Core_Gateway::start()
269
	 * @param Payment $payment Payment.
270
	 * @return void
271
	 * @throws Error Mollie error.
272
	 * @throws \Exception Throws exception on error creating Mollie customer for payment.
273
	 */
274
	public function start( Payment $payment ) {
275
		$description = (string) $payment->get_description();
276
277
		/**
278
		 * Filters the Mollie payment description.
279
		 * 
280
		 * The maximum length of the description field differs per payment
281
		 * method, with the absolute maximum being 255 characters.
282
		 *
283
		 * @link https://docs.mollie.com/reference/v2/payments-api/create-payment#parameters
284
		 * @since 3.0.1
285
		 * @param string  $description Description.
286
		 * @param Payment $payment     Payment.
287
		 */
288
		$description = \apply_filters( 'pronamic_pay_mollie_payment_description', $description, $payment );
289
290
		$request = new PaymentRequest(
291
			AmountTransformer::transform( $payment->get_total_amount() ),
292
			$description
293
		);
294
295
		$request->redirect_url = $payment->get_return_url();
296
		$request->webhook_url  = $this->get_webhook_url( $payment );
297
298
		// Locale.
299
		$customer = $payment->get_customer();
300
301
		if ( null !== $customer ) {
302
			$request->locale = LocaleHelper::transform( $customer->get_locale() );
303
		}
304
305
		// Customer ID.
306
		$customer_id = $this->get_customer_id_for_payment( $payment );
307
308
		if ( null === $customer_id ) {
309
			$customer_id = $this->create_customer_for_payment( $payment );
310
		}
311
312
		if ( null !== $customer_id ) {
313
			$request->customer_id = $customer_id;
314
		}
315
316
		/**
317
		 * Payment method.
318
		 *
319
		 * Leap of faith if the WordPress payment method could not transform to a Mollie method?
320
		 */
321
		$payment_method = $payment->get_payment_method();
322
323
		$request->set_method( Methods::transform( $payment_method, $payment_method ) );
324
325
		/**
326
		 * Sequence type.
327
		 *
328
		 * Recurring payments are created through the Payments API by providing a `sequenceType`.
329
		 */
330
		$subscriptions = $payment->get_subscriptions();
331
332
		if ( \count( $subscriptions ) > 0 ) {
333
			$first_method = PaymentMethods::get_first_payment_method( $payment_method );
334
335
			$request->set_method( Methods::transform( $first_method, $first_method ) );
336
337
			$request->set_sequence_type( 'first' );
338
339
			foreach ( $subscriptions as $subscription ) {
340
				$mandate_id = $subscription->get_meta( 'mollie_mandate_id' );
341
342
				if ( ! empty( $mandate_id ) ) {
343
					$request->set_mandate_id( $mandate_id );
344
				}
345
346
				if ( ! $subscription->is_first_payment( $payment ) ) {
347
					$request->set_method( null );
348
					$request->set_sequence_type( 'recurring' );
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( '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
		$attempt = (int) $payment->get_meta( 'mollie_create_payment_attempt' );
446
		$attempt = empty( $attempt ) ? 1 : $attempt + 1;
447
448
		$payment->set_meta( 'mollie_create_payment_attempt', $attempt );
449
450
		try {
451
			$result = $this->client->create_payment( $request );
452
453
			$payment->delete_meta( 'mollie_create_payment_attempt' );
454
		} catch ( Error $error ) {
455
			if ( 'recurring' !== $request->get_sequence_type() ) {
456
				throw $error;
457
			}
458
459
			if ( null === $request->get_mandate_id() ) {
460
				throw $error;
461
			}
462
463
			/**
464
			 * Only schedule retry for specific status codes.
465
			 *
466
			 * @link https://docs.mollie.com/overview/handling-errors
467
			 */
468
			if ( ! \in_array( $error->get_status(), array( 429, 502, 503 ), true ) ) {
469
				throw $error;
470
			}
471
472
			\as_schedule_single_action(
473
				\time() + $this->get_retry_seconds( $attempt ),
474
				'pronamic_pay_mollie_payment_start',
475
				array(
476
					'payment_id' => $payment->get_id(),
477
				),
478
				'pronamic-pay-mollie'
479
			);
480
481
			// Add note.
482
			$payment->add_note(
483
				\sprintf(
484
					'%s - %s - %s',
485
					$error->get_status(),
486
					$error->get_title(),
487
					$error->get_detail()
488
				) 
489
			);
490
491
			$payment->save();
492
493
			return;
494
		}
495
496
		// phpcs:disable WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase -- Mollie JSON object.
497
498
		// Set transaction ID.
499
		if ( isset( $result->id ) ) {
500
			$payment->set_transaction_id( $result->id );
501
		}
502
503
		// Set payment method.
504
		if ( isset( $result->method ) ) {
505
			$payment_method = Methods::transform_gateway_method( $result->method );
506
507
			if ( null !== $payment_method ) {
508
				$payment->set_payment_method( $payment_method );
509
510
				// Update subscription payment method.
511
				foreach ( $payment->get_subscriptions() as $subscription ) {
512
					if ( null === $subscription->get_payment_method() ) {
513
						$subscription->set_payment_method( $payment->get_payment_method() );
514
515
						$subscription->save();
516
					}
517
				}
518
			}
519
		}
520
521
		// Set expiry date.
522
		if ( isset( $result->expiresAt ) ) {
523
			try {
524
				$expires_at = new DateTime( $result->expiresAt );
525
			} catch ( \Exception $e ) {
526
				$expires_at = null;
527
			}
528
529
			$payment->set_expiry_date( $expires_at );
530
		}
531
532
		// Set status.
533
		if ( isset( $result->status ) ) {
534
			$payment->set_status( Statuses::transform( $result->status ) );
535
		}
536
537
		// Set bank transfer recipient details.
538
		if ( isset( $result->details ) ) {
539
			$bank_transfer_recipient_details = $payment->get_bank_transfer_recipient_details();
540
541
			if ( null === $bank_transfer_recipient_details ) {
542
				$bank_transfer_recipient_details = new BankTransferDetails();
543
544
				$payment->set_bank_transfer_recipient_details( $bank_transfer_recipient_details );
545
			}
546
547
			$bank_details = $bank_transfer_recipient_details->get_bank_account();
548
549
			if ( null === $bank_details ) {
550
				$bank_details = new BankAccountDetails();
551
552
				$bank_transfer_recipient_details->set_bank_account( $bank_details );
553
			}
554
555
			$details = $result->details;
556
557
			if ( isset( $details->bankName ) ) {
558
				/**
559
				 * Set `bankName` as bank details name, as result "Stichting Mollie Payments"
560
				 * is not the name of a bank, but the account holder name.
561
				 */
562
				$bank_details->set_name( $details->bankName );
563
			}
564
565
			if ( isset( $details->bankAccount ) ) {
566
				$bank_details->set_iban( $details->bankAccount );
567
			}
568
569
			if ( isset( $details->bankBic ) ) {
570
				$bank_details->set_bic( $details->bankBic );
571
			}
572
573
			if ( isset( $details->transferReference ) ) {
574
				$bank_transfer_recipient_details->set_reference( $details->transferReference );
575
			}
576
		}
577
578
		// Handle links.
579
		if ( isset( $result->_links ) ) {
580
			$links = $result->_links;
581
582
			// Action URL.
583
			if ( isset( $links->checkout->href ) ) {
584
				$payment->set_action_url( $links->checkout->href );
585
			}
586
587
			// Change payment state URL.
588
			if ( isset( $links->changePaymentState->href ) ) {
589
				$payment->set_meta( 'mollie_change_payment_state_url', $links->changePaymentState->href );
590
			}
591
		}
592
593
		// phpcs:enable WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase -- Mollie JSON object.
594
	}
595
596
	/**
597
	 * Get retry seconds.
598
	 *
599
	 * @param int $attempt Number of attempts.
600
	 * @return int
601
	 */
602
	private function get_retry_seconds( $attempt ) {
603
		switch ( $attempt ) {
604
			case 1:
605
				return 5 * MINUTE_IN_SECONDS;
606
			case 2:
607
				return HOUR_IN_SECONDS;
608
			case 3:
609
				return 12 * HOUR_IN_SECONDS;
610
			case 4:
611
			default:
612
				return DAY_IN_SECONDS;
613
		}
614
	}
615
616
	/**
617
	 * Update status of the specified payment
618
	 *
619
	 * @param Payment $payment Payment.
620
	 * @return void
621
	 */
622
	public function update_status( Payment $payment ) {
623
		$transaction_id = $payment->get_transaction_id();
624
625
		if ( null === $transaction_id ) {
626
			return;
627
		}
628
629
		$mollie_payment = $this->client->get_payment( $transaction_id );
630
631
		$payment->set_status( Statuses::transform( $mollie_payment->get_status() ) );
632
633
		/**
634
		 * Mollie profile.
635
		 */
636
		$mollie_profile = new Profile();
637
638
		$mollie_profile->set_id( $mollie_payment->get_profile_id() );
639
640
		$profile_internal_id = $this->profile_data_store->get_or_insert_profile( $mollie_profile );
641
642
		/**
643
		 * If the Mollie payment contains a customer ID we will try to connect
644
		 * this Mollie customer ID the WordPress user and subscription.
645
		 * This can be useful in case when a WordPress user is created after
646
		 * a successful payment.
647
		 *
648
		 * @link https://www.gravityforms.com/add-ons/user-registration/
649
		 */
650
		$mollie_customer_id = $mollie_payment->get_customer_id();
651
652
		if ( null !== $mollie_customer_id ) {
653
			$mollie_customer = new Customer( $mollie_customer_id );
654
655
			$customer_internal_id = $this->customer_data_store->get_or_insert_customer(
656
				$mollie_customer,
657
				array(
658
					'profile_id' => $profile_internal_id,
659
				),
660
				array(
661
					'profile_id' => '%s',
662
				)
663
			);
664
665
			// Customer.
666
			$customer = $payment->get_customer();
667
668
			if ( null !== $customer ) {
669
				// Connect to user.
670
				$user_id = $customer->get_user_id();
671
672
				if ( null !== $user_id ) {
673
					$user = \get_user_by( 'id', $user_id );
674
675
					if ( false !== $user ) {
676
						$this->customer_data_store->connect_mollie_customer_to_wp_user( $mollie_customer, $user );
677
					}
678
				}
679
			}
680
		}
681
682
		/**
683
		 * Customer ID.
684
		 */
685
		$mollie_customer_id = $mollie_payment->get_customer_id();
686
687
		if ( null !== $mollie_customer_id ) {
688
			$customer_id = $payment->get_meta( 'mollie_customer_id' );
689
690
			if ( empty( $customer_id ) ) {
691
				$payment->set_meta( 'mollie_customer_id', $mollie_customer_id );
692
			}
693
694
			foreach ( $payment->get_subscriptions() as $subscription ) {
695
				$customer_id = $subscription->get_meta( 'mollie_customer_id' );
696
697
				if ( empty( $customer_id ) ) {
698
					$subscription->set_meta( 'mollie_customer_id', $mollie_customer_id );
699
				}
700
			}
701
		}
702
703
		/**
704
		 * Mandate ID.
705
		 */
706
		$mollie_mandate_id = $mollie_payment->get_mandate_id();
707
708
		if ( null !== $mollie_mandate_id ) {
709
			$mandate_id = $payment->get_meta( 'mollie_mandate_id' );
710
711
			if ( empty( $mandate_id ) ) {
712
				$payment->set_meta( 'mollie_mandate_id', $mollie_mandate_id );
713
			}
714
715
			foreach ( $payment->get_subscriptions() as $subscription ) {
716
				$mandate_id = $subscription->get_meta( 'mollie_mandate_id' );
717
718
				if ( empty( $mandate_id ) ) {
719
					$subscription->set_meta( 'mollie_mandate_id', $mollie_mandate_id );
720
				}
721
			}
722
		}
723
724
		// Set payment method.
725
		$method = $mollie_payment->get_method();
726
727
		if ( null !== $method ) {
728
			$payment_method = Methods::transform_gateway_method( $method );
729
730
			if ( null !== $payment_method ) {
731
				$payment->set_payment_method( $payment_method );
732
733
				// Update subscription payment method.
734
				foreach ( $payment->get_subscriptions() as $subscription ) {
735
					if ( null === $subscription->get_payment_method() ) {
736
						$subscription->set_payment_method( $payment->get_payment_method() );
737
738
						$subscription->save();
739
					}
740
				}
741
			}
742
		}
743
744
		// phpcs:disable WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase -- Mollie JSON object.
745
		$mollie_payment_details = $mollie_payment->get_details();
746
747
		if ( null !== $mollie_payment_details ) {
748
			$consumer_bank_details = $payment->get_consumer_bank_details();
749
750
			if ( null === $consumer_bank_details ) {
751
				$consumer_bank_details = new BankAccountDetails();
752
753
				$payment->set_consumer_bank_details( $consumer_bank_details );
754
			}
755
756
			if ( isset( $mollie_payment_details->consumerName ) ) {
757
				$consumer_bank_details->set_name( $mollie_payment_details->consumerName );
758
			}
759
760
			if ( isset( $mollie_payment_details->cardHolder ) ) {
761
				$consumer_bank_details->set_name( $mollie_payment_details->cardHolder );
762
			}
763
764
			if ( isset( $mollie_payment_details->cardNumber ) ) {
765
				// The last four digits of the card number.
766
				$consumer_bank_details->set_account_number( $mollie_payment_details->cardNumber );
767
			}
768
769
			if ( isset( $mollie_payment_details->cardCountryCode ) ) {
770
				// The ISO 3166-1 alpha-2 country code of the country the card was issued in.
771
				$consumer_bank_details->set_country( $mollie_payment_details->cardCountryCode );
772
			}
773
774
			if ( isset( $mollie_payment_details->consumerAccount ) ) {
775
				switch ( $mollie_payment->get_method() ) {
776
					case Methods::BELFIUS:
777
					case Methods::DIRECT_DEBIT:
778
					case Methods::IDEAL:
779
					case Methods::KBC:
780
					case Methods::SOFORT:
781
						$consumer_bank_details->set_iban( $mollie_payment_details->consumerAccount );
782
783
						break;
784
					case Methods::BANCONTACT:
785
					case Methods::BANKTRANSFER:
786
					case Methods::PAYPAL:
787
					default:
788
						$consumer_bank_details->set_account_number( $mollie_payment_details->consumerAccount );
789
790
						break;
791
				}
792
			}
793
794
			if ( isset( $mollie_payment_details->consumerBic ) ) {
795
				$consumer_bank_details->set_bic( $mollie_payment_details->consumerBic );
796
			}
797
798
			/*
799
			 * Failure reason.
800
			 */
801
			$failure_reason = $payment->get_failure_reason();
802
803
			if ( null === $failure_reason ) {
804
				$failure_reason = new FailureReason();
805
			}
806
807
			// SEPA Direct Debit.
808
			if ( isset( $mollie_payment_details->bankReasonCode ) ) {
809
				$failure_reason->set_code( $mollie_payment_details->bankReasonCode );
810
			}
811
812
			if ( isset( $mollie_payment_details->bankReason ) ) {
813
				$failure_reason->set_message( $mollie_payment_details->bankReason );
814
			}
815
816
			// Credit card.
817
			if ( isset( $mollie_payment_details->failureReason ) ) {
818
				$failure_reason->set_code( $mollie_payment_details->failureReason );
819
			}
820
821
			if ( isset( $mollie_payment_details->failureMessage ) ) {
822
				$failure_reason->set_message( $mollie_payment_details->failureMessage );
823
			}
824
825
			$failure_code    = $failure_reason->get_code();
826
			$failure_message = $failure_reason->get_message();
827
828
			if ( ! empty( $failure_code ) || ! empty( $failure_message ) ) {
829
				$payment->set_failure_reason( $failure_reason );
830
			}
831
		}
832
833
		$links = $mollie_payment->get_links();
834
835
		// Change payment state URL.
836
		if ( \property_exists( $links, 'changePaymentState' ) ) {
837
			$payment->set_meta( 'mollie_change_payment_state_url', $links->changePaymentState->href );
838
		}
839
840
		// phpcs:enable WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase -- Mollie JSON object.
841
842
		if ( $mollie_payment->has_chargebacks() ) {
843
			$mollie_chargebacks = $this->client->get_payment_chargebacks(
844
				$mollie_payment->get_id(),
845
				array( 'limit' => 1 )
846
			);
847
848
			$mollie_chargeback = \reset( $mollie_chargebacks );
849
850
			if ( false !== $mollie_chargeback ) {
851
				$subscriptions = array_filter(
852
					$payment->get_subscriptions(),
853
					function ( $subscription ) {
854
						return SubscriptionStatus::ACTIVE === $subscription->get_status();
855
					}
856
				);
857
858
				foreach ( $subscriptions as $subscription ) {
859
					if ( $mollie_chargeback->get_created_at() > $subscription->get_activated_at() ) {
860
						$subscription->set_status( SubscriptionStatus::ON_HOLD );
861
862
						$subscription->add_note(
863
							\sprintf(
864
								/* translators: 1: Mollie chargeback ID, 2: Mollie payment ID */
865
								\__( 'Subscription put on hold due to chargeback `%1$s` of payment `%2$s`.', 'pronamic_ideal' ),
866
								\esc_html( $mollie_chargeback->get_id() ),
867
								\esc_html( $mollie_payment->get_id() )
868
							)
869
						);
870
					}
871
				}
872
			}
873
		}
874
875
		// Refunds.
876
		$amount_refunded = $mollie_payment->get_amount_refunded();
877
878
		if ( null !== $amount_refunded ) {
879
			$refunded_amount = new Money( $amount_refunded->get_value(), $amount_refunded->get_currency() );
880
881 10
			$payment->set_refunded_amount( $refunded_amount->get_value() > 0 ? $refunded_amount : null );
882 10
		}
883
884 10
		// Save.
885
		$payment->save();
886 10
887
		foreach ( $payment->get_subscriptions() as $subscription ) {
888
			$subscription->save();
889
		}
890
	}
891
892
	/**
893
	 * Update subscription mandate.
894
	 *
895 10
	 * @param Subscription $subscription   Subscription.
896 10
	 * @param string       $mandate_id     Mollie mandate ID.
897
	 * @param string|null  $payment_method Payment method.
898
	 * @return void
899 10
	 * @throws \Exception Throws exception if subscription note could not be added.
900
	 */
901 10
	public function update_subscription_mandate( Subscription $subscription, $mandate_id, $payment_method = null ) {
902 10
		$customer_id = (string) $subscription->get_meta( 'mollie_customer_id' );
903
904 10
		$mandate = $this->client->get_mandate( $mandate_id, $customer_id );
905 4
906
		if ( ! \is_object( $mandate ) ) {
907
			return;
908
		}
909
910 10
		// Update mandate.
911
		$old_mandate_id = $subscription->get_meta( 'mollie_mandate_id' );
912 10
913 10
		$subscription->set_meta( 'mollie_mandate_id', $mandate_id );
914
915 10
		if ( ! empty( $old_mandate_id ) && $old_mandate_id !== $mandate_id ) {
916 7
			$note = \sprintf(
917
			/* translators: 1: old mandate ID, 2: new mandate ID */
918 7
				\__( 'Mandate for subscription changed from "%1$s" to "%2$s".', 'pronamic_ideal' ),
919
				\esc_html( $old_mandate_id ),
920
				\esc_html( $mandate_id )
921
			);
922 10
923
			$subscription->add_note( $note );
924
		}
925
926
		// Update payment method.
927
		$old_method = $subscription->get_payment_method();
928
		$new_method = ( null === $payment_method && \property_exists( $mandate, 'method' ) ? Methods::transform_gateway_method( $mandate->method ) : $payment_method );
929
930
		// `Direct Debit` is not a recurring method, use `Direct Debit (mandate via ...)` instead.
931 24
		if ( PaymentMethods::DIRECT_DEBIT === $new_method ) {
932 24
			$new_method = PaymentMethods::DIRECT_DEBIT_IDEAL;
933
934 24
			// Use `Direct Debit (mandate via Bancontact)` if consumer account starts with `BE`.
935
			if ( \property_exists( $mandate, 'details' ) && 'BE' === \substr( $mandate->details->consumerAccount, 0, 2 ) ) {
936
				$new_method = PaymentMethods::DIRECT_DEBIT_BANCONTACT;
937
			}
938 24
		}
939
940 24
		if ( ! empty( $old_method ) && $old_method !== $new_method ) {
941
			$subscription->set_payment_method( $new_method );
942 24
943
			// Add note.
944
			$note = \sprintf(
945
				/* translators: 1: old payment method, 2: new payment method */
946
				\__( 'Payment method for subscription changed from "%1$s" to "%2$s".', 'pronamic_ideal' ),
947
				\esc_html( (string) PaymentMethods::get_name( $old_method ) ),
948
				\esc_html( (string) PaymentMethods::get_name( $new_method ) )
949
			);
950
951 10
			$subscription->add_note( $note );
952 10
		}
953
954 10
		$subscription->save();
955
	}
956 7
957
	/**
958 7
	 * Create refund.
959 7
	 *
960
	 * @param string $transaction_id Transaction ID.
961
	 * @param Money  $amount         Amount to refund.
962
	 * @param string $description    Refund reason.
963 10
	 * @return string
964 6
	 */
965
	public function create_refund( $transaction_id, Money $amount, $description = null ) {
966
		$request = new RefundRequest( AmountTransformer::transform( $amount ) );
967 4
968
		// Metadata payment ID.
969
		$payment = \get_pronamic_payment_by_transaction_id( $transaction_id );
970
971
		if ( null !== $payment ) {
972
			$request->set_metadata(
973
				array(
974
					'pronamic_payment_id' => $payment->get_id(),
975
				)
976
			);
977 10
		}
978 10
979
		// Description.
980 10
		if ( ! empty( $description ) ) {
981
			$request->set_description( $description );
982 10
		}
983
984 4
		$refund = $this->client->create_refund( $transaction_id, $request );
985
986
		return $refund->get_id();
987
	}
988
989
	/**
990
	 * Get Mollie customer ID for payment.
991
	 *
992
	 * @param Payment $payment Payment.
993
	 * @return string|null
994 4
	 */
995 4
	public function get_customer_id_for_payment( Payment $payment ) {
996
		$customer_ids = $this->get_customer_ids_for_payment( $payment );
997
998
		$customer_id = $this->get_first_existing_customer_id( $customer_ids );
999 6
1000
		return $customer_id;
1001
	}
1002
1003
	/**
1004
	 * Get Mollie customers for the specified payment.
1005
	 *
1006
	 * @param Payment $payment Payment.
1007
	 * @return array<string>
1008
	 */
1009
	private function get_customer_ids_for_payment( Payment $payment ) {
1010
		$customer_ids = array();
1011
1012
		// Customer ID from subscription meta.
1013
		$subscription = $payment->get_subscription();
1014
1015
		if ( null !== $subscription ) {
1016
			$customer_id = $this->get_customer_id_for_subscription( $subscription );
1017
1018
			if ( null !== $customer_id ) {
1019
				$customer_ids[] = $customer_id;
1020
			}
1021
		}
1022
1023
		// Customer ID from WordPress user.
1024
		$customer = $payment->get_customer();
1025
1026
		if ( null !== $customer ) {
1027
			$user_id = $customer->get_user_id();
1028
1029
			if ( ! empty( $user_id ) ) {
1030
				$user_customer_ids = $this->get_customer_ids_for_user( $user_id );
1031
1032
				$customer_ids = \array_merge( $customer_ids, $user_customer_ids );
1033
			}
1034
		}
1035
1036
		return $customer_ids;
1037
	}
1038
1039
	/**
1040
	 * Get Mollie customers for the specified WordPress user ID.
1041
	 *
1042
	 * @param int $user_id WordPress user ID.
1043
	 * @return array<string>
1044
	 */
1045
	public function get_customer_ids_for_user( $user_id ) {
1046
		$customer_query = new CustomerQuery(
1047
			array(
1048
				'user_id' => $user_id,
1049
			)
1050
		);
1051
1052
		$customers = $customer_query->get_customers();
1053
1054
		$customer_ids = wp_list_pluck( $customers, 'mollie_id' );
1055
1056
		return $customer_ids;
1057
	}
1058
1059
	/**
1060
	 * Get customer ID for subscription.
1061
	 *
1062
	 * @param Subscription $subscription Subscription.
1063
	 * @return string|null
1064
	 */
1065
	private function get_customer_id_for_subscription( Subscription $subscription ) {
1066
		$customer_id = $subscription->get_meta( 'mollie_customer_id' );
1067
1068
		if ( empty( $customer_id ) ) {
1069
			// Try to get (legacy) customer ID from first payment.
1070
			$first_payment = $subscription->get_first_payment();
1071
1072
			if ( null !== $first_payment ) {
1073
				$customer_id = $first_payment->get_meta( 'mollie_customer_id' );
1074
			}
1075
		}
1076
1077
		if ( empty( $customer_id ) ) {
1078 27
			return null;
1079 27
		}
1080 1
1081
		return $customer_id;
1082
	}
1083
1084 26
	/**
1085
	 * Get first existing customer from customers list.
1086
	 *
1087 26
	 * @param array<string> $customer_ids Customers.
1088
	 * @return string|null
1089 26
	 * @throws Error Throws error on Mollie error.
1090 16
	 */
1091
	private function get_first_existing_customer_id( $customer_ids ) {
1092
		$customer_ids = \array_filter( $customer_ids );
1093 26
1094
		$customer_ids = \array_unique( $customer_ids );
1095
1096
		foreach ( $customer_ids as $customer_id ) {
1097
			try {
1098 26
				$customer = $this->client->get_customer( $customer_id );
1099
			} catch ( Error $error ) {
1100 26
				// Check for status 410 ("Gone - The customer is no longer available").
1101 3
				if ( 410 === $error->get_status() ) {
1102
					continue;
1103
				}
1104 23
1105
				throw $error;
1106 23
			}
1107 12
1108
			if ( null !== $customer ) {
1109
				return $customer_id;
1110
			}
1111 11
		}
1112
1113
		return null;
1114 11
	}
1115
1116
	/**
1117 11
	 * Create customer for payment.
1118 11
	 *
1119
	 * @param Payment $payment Payment.
1120
	 * @return string|null
1121
	 * @throws Error Throws Error when Mollie error occurs.
1122 11
	 * @throws \Exception Throws exception when error in customer data store occurs.
1123 11
	 */
1124
	private function create_customer_for_payment( Payment $payment ) {
1125 11
		$mollie_customer = new Customer();
1126 2
		$mollie_customer->set_mode( $this->config->is_test_mode() ? 'test' : 'live' );
1127
		$mollie_customer->set_email( $payment->get_email() );
1128 2
1129
		$pronamic_customer = $payment->get_customer();
1130 2
1131
		if ( null !== $pronamic_customer ) {
1132 11
			// Name.
1133
			$name = (string) $pronamic_customer->get_name();
1134
1135
			if ( '' !== $name ) {
1136
				$mollie_customer->set_name( $name );
1137
			}
1138
1139
			// Locale.
1140
			$locale = $pronamic_customer->get_locale();
1141
1142
			if ( null !== $locale ) {
1143
				$mollie_customer->set_locale( LocaleHelper::transform( $locale ) );
1144
			}
1145
		}
1146
1147
		// Try to get name from consumer bank details.
1148
		$consumer_bank_details = $payment->get_consumer_bank_details();
1149
1150
		if ( null === $mollie_customer->get_name() && null !== $consumer_bank_details ) {
1151
			$name = $consumer_bank_details->get_name();
1152
1153
			if ( null !== $name ) {
1154
				$mollie_customer->set_name( $name );
1155
			}
1156
		}
1157
1158
		// Create customer.
1159
		$mollie_customer = $this->client->create_customer( $mollie_customer );
1160
1161
		$customer_id = $this->customer_data_store->insert_customer( $mollie_customer );
1162
1163
		// Connect to user.
1164
		if ( null !== $pronamic_customer ) {
1165
			$user_id = $pronamic_customer->get_user_id();
1166
1167
			if ( null !== $user_id ) {
1168
				$user = \get_user_by( 'id', $user_id );
1169
1170
				if ( false !== $user ) {
1171
					$this->customer_data_store->connect_mollie_customer_to_wp_user( $mollie_customer, $user );
1172
				}
1173
			}
1174
		}
1175
1176
		// Store customer ID in subscription meta.
1177
		$subscription = $payment->get_subscription();
1178
1179
		if ( null !== $subscription ) {
1180
			$subscription->set_meta( 'mollie_customer_id', $mollie_customer->get_id() );
1181
		}
1182
1183
		return $mollie_customer->get_id();
1184
	}
1185
1186
	/**
1187
	 * Copy Mollie customer ID from subscription meta to WordPress user meta.
1188
	 *
1189
	 * @param Payment $payment Payment.
1190
	 * @return void
1191
	 */
1192
	public function copy_customer_id_to_wp_user( Payment $payment ) {
1193
		if ( $this->config->id !== $payment->config_id ) {
1194
			return;
1195
		}
1196
1197
		// Subscription.
1198
		$subscription = $payment->get_subscription();
1199
1200
		// Customer.
1201
		$customer = $payment->get_customer();
1202
1203
		if ( null === $customer && null !== $subscription ) {
1204
			$customer = $subscription->get_customer();
1205
		}
1206
1207
		if ( null === $customer ) {
1208
			return;
1209
		}
1210
1211
		// WordPress user.
1212
		$user_id = $customer->get_user_id();
1213
1214
		if ( null === $user_id ) {
1215
			return;
1216
		}
1217
1218
		$user = \get_user_by( 'id', $user_id );
1219
1220
		if ( false === $user ) {
1221
			return;
1222
		}
1223
1224
		// Customer IDs.
1225
		$customer_ids = array();
1226
1227
		// Payment.
1228
		$customer_ids[] = $payment->get_meta( 'mollie_customer_id' );
1229
1230
		// Subscription.
1231
		if ( null !== $subscription ) {
1232
			$customer_ids[] = $subscription->get_meta( 'mollie_customer_id' );
1233
		}
1234
1235
		// Connect.
1236
		$customer_ids = \array_filter( $customer_ids );
1237
		$customer_ids = \array_unique( $customer_ids );
1238
1239
		foreach ( $customer_ids as $customer_id ) {
1240
			$customer = new Customer( $customer_id );
1241
1242
			$this->customer_data_store->get_or_insert_customer( $customer );
1243
1244
			$this->customer_data_store->connect_mollie_customer_to_wp_user( $customer, $user );
1245
		}
1246
	}
1247
}
1248