Test Failed
Push — develop ( f1e77e...745105 )
by Remco
05:28
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\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
	 * @param Payment $payment Payment.
237
	 * @return string|null
238 4
	 */
239 4
	public function get_webhook_url( Payment $payment ) {
240
		$url = \rest_url( Integration::REST_ROUTE_NAMESPACE . '/webhook/' . (string) $payment->get_id() );
241 4
242
		$host = wp_parse_url( $url, PHP_URL_HOST );
243 4
244
		if ( is_array( $host ) ) {
245
			// Parsing failure.
246
			$host = '';
247
		}
248 4
249
		if ( 'localhost' === $host ) {
250 1
			// Mollie doesn't allow localhost.
251 3
			return null;
252
		} elseif ( '.dev' === substr( $host, -4 ) ) {
253 1
			// Mollie doesn't allow the .dev TLD.
254 2
			return null;
255
		} elseif ( '.local' === substr( $host, -6 ) ) {
256 1
			// Mollie doesn't allow the .local TLD.
257 1
			return null;
258
		} elseif ( '.test' === substr( $host, -5 ) ) {
259
			// Mollie doesn't allow the .test TLD.
260
			return null;
261
		}
262 1
263
		return $url;
264
	}
265
266
	/**
267
	 * Start
268
	 *
269
	 * @see Core_Gateway::start()
270
	 * @param Payment $payment Payment.
271
	 * @return void
272
	 * @throws Error Mollie error.
273
	 * @throws \Exception Throws exception on error creating Mollie customer for payment.
274
	 */
275
	public function start( Payment $payment ) {
276
		$description = (string) $payment->get_description();
277
278
		/**
279
		 * Filters the Mollie payment description.
280
		 * 
281
		 * The maximum length of the description field differs per payment
282
		 * method, with the absolute maximum being 255 characters.
283
		 *
284
		 * @link https://docs.mollie.com/reference/v2/payments-api/create-payment#parameters
285
		 * @since 3.0.1
286
		 * @param string  $description Description.
287
		 * @param Payment $payment     Payment.
288
		 */
289
		$description = \apply_filters( 'pronamic_pay_mollie_payment_description', $description, $payment );
290
291
		$request = new PaymentRequest(
292
			AmountTransformer::transform( $payment->get_total_amount() ),
293
			$description
294
		);
295
296
		$request->redirect_url = $payment->get_return_url();
297
		$request->webhook_url  = $this->get_webhook_url( $payment );
298
299
		// Locale.
300
		$customer = $payment->get_customer();
301
302
		if ( null !== $customer ) {
303
			$request->locale = LocaleHelper::transform( $customer->get_locale() );
304
		}
305
306
		// Customer ID.
307
		$customer_id = $this->get_customer_id_for_payment( $payment );
308
309
		if ( null === $customer_id ) {
310
			$customer_id = $this->create_customer_for_payment( $payment );
311
		}
312
313
		if ( null !== $customer_id ) {
314
			$request->customer_id = $customer_id;
315
		}
316
317
		/**
318
		 * Payment method.
319
		 *
320
		 * Leap of faith if the WordPress payment method could not transform to a Mollie method?
321
		 */
322
		$payment_method = $payment->get_payment_method();
323
324
		$request->set_method( Methods::transform( $payment_method, $payment_method ) );
325
326
		/**
327
		 * Sequence type.
328
		 *
329
		 * Recurring payments are created through the Payments API by providing a `sequenceType`.
330
		 */
331
		$subscriptions = $payment->get_subscriptions();
332
333
		if ( \count( $subscriptions ) > 0 ) {
334
			$first_method = PaymentMethods::get_first_payment_method( $payment_method );
335
336
			$request->set_method( Methods::transform( $first_method, $first_method ) );
337
338
			$request->set_sequence_type( 'first' );
339
340
			foreach ( $subscriptions as $subscription ) {
341
				$mandate_id = $subscription->get_meta( 'mollie_mandate_id' );
342
343
				if ( ! empty( $mandate_id ) ) {
344
					$request->set_mandate_id( $mandate_id );
345
				}
346
347
				if ( ! $subscription->is_first_payment( $payment ) ) {
348
					$request->set_method( null );
349
					$request->set_sequence_type( 'recurring' );
350
				}
351
			}
352
		}
353
354
		/**
355
		 * Direct Debit.
356
		 *
357
		 * Check if one-off SEPA Direct Debit can be used, otherwise short circuit payment.
358
		 */
359
		$consumer_bank_details = $payment->get_consumer_bank_details();
360
361
		if ( PaymentMethods::DIRECT_DEBIT === $payment_method && null !== $consumer_bank_details ) {
362
			$consumer_name = $consumer_bank_details->get_name();
363
			$consumer_iban = $consumer_bank_details->get_iban();
364
365
			$request->consumer_name    = $consumer_name;
366
			$request->consumer_account = $consumer_iban;
367
368
			// Check if one-off SEPA Direct Debit can be used, otherwise short circuit payment.
369
			if ( null !== $customer_id ) {
370
				// Find or create mandate.
371
				$mandate_id = $this->client->has_valid_mandate( $customer_id, PaymentMethods::DIRECT_DEBIT, $consumer_iban );
372
373
				if ( false === $mandate_id ) {
374
					$mandate = $this->client->create_mandate( $customer_id, $consumer_bank_details );
375
376
					if ( ! \property_exists( $mandate, 'id' ) ) {
377
						throw new \Exception( 'Missing mandate ID.' );
378
					}
379
380
					$mandate_id = $mandate->id;
381
				}
382
383
				// Charge immediately on-demand.
384
				$request->set_sequence_type( 'recurring' );
385
				$request->set_mandate_id( (string) $mandate_id );
386
			}
387
		}
388
389
		/**
390
		 * Metadata.
391
		 *
392
		 * Provide any data you like, for example a string or a JSON object.
393
		 * We will save the data alongside the payment. Whenever you fetch
394
		 * the payment with our API, we’ll also include the metadata. You
395
		 * can use up to approximately 1kB.
396
		 *
397
		 * @link https://docs.mollie.com/reference/v2/payments-api/create-payment
398
		 * @link https://en.wikipedia.org/wiki/Metadata
399
		 */
400
		$metadata = null;
401
402
		/**
403
		 * Filters the Mollie metadata.
404
		 *
405
		 * @since 2.2.0
406
		 *
407
		 * @param mixed   $metadata Metadata.
408
		 * @param Payment $payment  Payment.
409
		 */
410
		$metadata = \apply_filters( 'pronamic_pay_mollie_payment_metadata', $metadata, $payment );
411
412
		$request->set_metadata( $metadata );
413
414
		// Issuer.
415
		if ( Methods::IDEAL === $request->method ) {
416
			$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...
417
		}
418
419
		// Billing email.
420
		$billing_email = $payment->get_email();
421
422
		/**
423
		 * Filters the Mollie payment billing email used for bank transfer payment instructions.
424
		 *
425
		 * @since 2.2.0
426
		 *
427
		 * @param string|null $billing_email Billing email.
428
		 * @param Payment     $payment       Payment.
429
		 */
430
		$billing_email = \apply_filters( 'pronamic_pay_mollie_payment_billing_email', $billing_email, $payment );
431
432
		$request->set_billing_email( $billing_email );
433
434
		// Due date.
435
		if ( ! empty( $this->config->due_date_days ) ) {
436
			try {
437
				$due_date = new DateTime( sprintf( '+%s days', $this->config->due_date_days ) );
438
			} catch ( \Exception $e ) {
439
				$due_date = null;
440
			}
441
442
			$request->set_due_date( $due_date );
443
		}
444
445
		// Create payment.
446
		$attempt = (int) $payment->get_meta( 'mollie_create_payment_attempt' );
447
		$attempt = empty( $attempt ) ? 1 : $attempt + 1;
448
449
		$payment->set_meta( 'mollie_create_payment_attempt', $attempt );
450
451
		try {
452
			$result = $this->client->create_payment( $request );
453
454
			$payment->delete_meta( 'mollie_create_payment_attempt' );
455
		} catch ( Error $error ) {
456
			if ( 'recurring' !== $request->get_sequence_type() ) {
457
				throw $error;
458
			}
459
460
			if ( null === $request->get_mandate_id() ) {
461
				throw $error;
462
			}
463
464
			/**
465
			 * Only schedule retry for specific status codes.
466
			 *
467
			 * @link https://docs.mollie.com/overview/handling-errors
468
			 */
469
			if ( ! \in_array( $error->get_status(), array( 429, 502, 503 ), true ) ) {
470
				throw $error;
471
			}
472
473
			\as_schedule_single_action(
474
				\time() + $this->get_retry_seconds( $attempt ),
475
				'pronamic_pay_mollie_payment_start',
476
				array(
477
					'payment_id' => $payment->get_id(),
478
				),
479
				'pronamic-pay-mollie'
480
			);
481
482
			// Add note.
483
			$payment->add_note(
484
				\sprintf(
485
					'%s - %s - %s',
486
					$error->get_status(),
487
					$error->get_title(),
488
					$error->get_detail()
489
				) 
490
			);
491
492
			$payment->save();
493
494
			return;
495
		}
496
497
		// phpcs:disable WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase -- Mollie JSON object.
498
499
		// Set transaction ID.
500
		if ( isset( $result->id ) ) {
501
			$payment->set_transaction_id( $result->id );
502
		}
503
504
		// Set payment method.
505
		if ( isset( $result->method ) ) {
506
			$payment_method = Methods::transform_gateway_method( $result->method );
507
508
			if ( null !== $payment_method ) {
509
				$payment->set_payment_method( $payment_method );
510
511
				// Update subscription payment method.
512
				foreach ( $payment->get_subscriptions() as $subscription ) {
513
					if ( null === $subscription->get_payment_method() ) {
514
						$subscription->set_payment_method( $payment->get_payment_method() );
515
516
						$subscription->save();
517
					}
518
				}
519
			}
520
		}
521
522
		// Set expiry date.
523
		if ( isset( $result->expiresAt ) ) {
524
			try {
525
				$expires_at = new DateTime( $result->expiresAt );
526
			} catch ( \Exception $e ) {
527
				$expires_at = null;
528
			}
529
530
			$payment->set_expiry_date( $expires_at );
531
		}
532
533
		// Set status.
534
		if ( isset( $result->status ) ) {
535
			$payment->set_status( Statuses::transform( $result->status ) );
536
		}
537
538
		// Set bank transfer recipient details.
539
		if ( isset( $result->details ) ) {
540
			$bank_transfer_recipient_details = $payment->get_bank_transfer_recipient_details();
541
542
			if ( null === $bank_transfer_recipient_details ) {
543
				$bank_transfer_recipient_details = new BankTransferDetails();
544
545
				$payment->set_bank_transfer_recipient_details( $bank_transfer_recipient_details );
546
			}
547
548
			$bank_details = $bank_transfer_recipient_details->get_bank_account();
549
550
			if ( null === $bank_details ) {
551
				$bank_details = new BankAccountDetails();
552
553
				$bank_transfer_recipient_details->set_bank_account( $bank_details );
554
			}
555
556
			$details = $result->details;
557
558
			if ( isset( $details->bankName ) ) {
559
				/**
560
				 * Set `bankName` as bank details name, as result "Stichting Mollie Payments"
561
				 * is not the name of a bank, but the account holder name.
562
				 */
563
				$bank_details->set_name( $details->bankName );
564
			}
565
566
			if ( isset( $details->bankAccount ) ) {
567
				$bank_details->set_iban( $details->bankAccount );
568
			}
569
570
			if ( isset( $details->bankBic ) ) {
571
				$bank_details->set_bic( $details->bankBic );
572
			}
573
574
			if ( isset( $details->transferReference ) ) {
575
				$bank_transfer_recipient_details->set_reference( $details->transferReference );
576
			}
577
		}
578
579
		// Handle links.
580
		if ( isset( $result->_links ) ) {
581
			$links = $result->_links;
582
583
			// Action URL.
584
			if ( isset( $links->checkout->href ) ) {
585
				$payment->set_action_url( $links->checkout->href );
586
			}
587
588
			// Change payment state URL.
589
			if ( isset( $links->changePaymentState->href ) ) {
590
				$payment->set_meta( 'mollie_change_payment_state_url', $links->changePaymentState->href );
591
			}
592
		}
593
594
		// phpcs:enable WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase -- Mollie JSON object.
595
	}
596
597
	/**
598
	 * Get retry seconds.
599
	 *
600
	 * @param int $attempt Number of attempts.
601
	 * @return int
602
	 */
603
	private function get_retry_seconds( $attempt ) {
604
		switch ( $attempt ) {
605
			case 1:
606
				return 5 * MINUTE_IN_SECONDS;
607
			case 2:
608
				return HOUR_IN_SECONDS;
609
			case 3:
610
				return 12 * HOUR_IN_SECONDS;
611
			case 4:
612
			default:
613
				return DAY_IN_SECONDS;
614
		}
615
	}
616
617
	/**
618
	 * Update status of the specified payment
619
	 *
620
	 * @param Payment $payment Payment.
621
	 * @return void
622
	 */
623
	public function update_status( Payment $payment ) {
624
		$transaction_id = $payment->get_transaction_id();
625
626
		if ( null === $transaction_id ) {
627
			return;
628
		}
629
630
		$mollie_payment = $this->client->get_payment( $transaction_id );
631
632
		$payment->set_status( Statuses::transform( $mollie_payment->get_status() ) );
633
634
		/**
635
		 * Mollie profile.
636
		 */
637
		$mollie_profile = new Profile();
638
639
		$mollie_profile->set_id( $mollie_payment->get_profile_id() );
640
641
		$profile_internal_id = $this->profile_data_store->get_or_insert_profile( $mollie_profile );
642
643
		/**
644
		 * If the Mollie payment contains a customer ID we will try to connect
645
		 * this Mollie customer ID the WordPress user and subscription.
646
		 * This can be useful in case when a WordPress user is created after
647
		 * a successful payment.
648
		 *
649
		 * @link https://www.gravityforms.com/add-ons/user-registration/
650
		 */
651
		$mollie_customer_id = $mollie_payment->get_customer_id();
652
653
		if ( null !== $mollie_customer_id ) {
654
			$mollie_customer = new Customer( $mollie_customer_id );
655
656
			$customer_internal_id = $this->customer_data_store->get_or_insert_customer(
657
				$mollie_customer,
658
				array(
659
					'profile_id' => $profile_internal_id,
660
				),
661
				array(
662
					'profile_id' => '%s',
663
				)
664
			);
665
666
			// Customer.
667
			$customer = $payment->get_customer();
668
669
			if ( null !== $customer ) {
670
				// Connect to user.
671
				$user_id = $customer->get_user_id();
672
673
				if ( null !== $user_id ) {
674
					$user = \get_user_by( 'id', $user_id );
675
676
					if ( false !== $user ) {
677
						$this->customer_data_store->connect_mollie_customer_to_wp_user( $mollie_customer, $user );
678
					}
679
				}
680
			}
681
		}
682
683
		/**
684
		 * Customer ID.
685
		 */
686
		$mollie_customer_id = $mollie_payment->get_customer_id();
687
688
		if ( null !== $mollie_customer_id ) {
689
			$customer_id = $payment->get_meta( 'mollie_customer_id' );
690
691
			if ( empty( $customer_id ) ) {
692
				$payment->set_meta( 'mollie_customer_id', $mollie_customer_id );
693
			}
694
695
			foreach ( $payment->get_subscriptions() as $subscription ) {
696
				$customer_id = $subscription->get_meta( 'mollie_customer_id' );
697
698
				if ( empty( $customer_id ) ) {
699
					$subscription->set_meta( 'mollie_customer_id', $mollie_customer_id );
700
				}
701
			}
702
		}
703
704
		/**
705
		 * Mandate ID.
706
		 */
707
		$mollie_mandate_id = $mollie_payment->get_mandate_id();
708
709
		if ( null !== $mollie_mandate_id ) {
710
			$mandate_id = $payment->get_meta( 'mollie_mandate_id' );
711
712
			if ( empty( $mandate_id ) ) {
713
				$payment->set_meta( 'mollie_mandate_id', $mollie_mandate_id );
714
			}
715
716
			foreach ( $payment->get_subscriptions() as $subscription ) {
717
				$mandate_id = $subscription->get_meta( 'mollie_mandate_id' );
718
719
				if ( empty( $mandate_id ) ) {
720
					$subscription->set_meta( 'mollie_mandate_id', $mollie_mandate_id );
721
				}
722
			}
723
		}
724
725
		// Set payment method.
726
		$method = $mollie_payment->get_method();
727
728
		if ( null !== $method ) {
729
			$payment_method = Methods::transform_gateway_method( $method );
730
731
			if ( null !== $payment_method ) {
732
				$payment->set_payment_method( $payment_method );
733
734
				// Update subscription payment method.
735
				foreach ( $payment->get_subscriptions() as $subscription ) {
736
					if ( null === $subscription->get_payment_method() ) {
737
						$subscription->set_payment_method( $payment->get_payment_method() );
738
739
						$subscription->save();
740
					}
741
				}
742
			}
743
		}
744
745
		// phpcs:disable WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase -- Mollie JSON object.
746
		$mollie_payment_details = $mollie_payment->get_details();
747
748
		if ( null !== $mollie_payment_details ) {
749
			$consumer_bank_details = $payment->get_consumer_bank_details();
750
751
			if ( null === $consumer_bank_details ) {
752
				$consumer_bank_details = new BankAccountDetails();
753
754
				$payment->set_consumer_bank_details( $consumer_bank_details );
755
			}
756
757
			if ( isset( $mollie_payment_details->consumerName ) ) {
758
				$consumer_bank_details->set_name( $mollie_payment_details->consumerName );
759
			}
760
761
			if ( isset( $mollie_payment_details->cardHolder ) ) {
762
				$consumer_bank_details->set_name( $mollie_payment_details->cardHolder );
763
			}
764
765
			if ( isset( $mollie_payment_details->cardNumber ) ) {
766
				// The last four digits of the card number.
767
				$consumer_bank_details->set_account_number( $mollie_payment_details->cardNumber );
768
			}
769
770
			if ( isset( $mollie_payment_details->cardCountryCode ) ) {
771
				// The ISO 3166-1 alpha-2 country code of the country the card was issued in.
772
				$consumer_bank_details->set_country( $mollie_payment_details->cardCountryCode );
773
			}
774
775
			if ( isset( $mollie_payment_details->consumerAccount ) ) {
776
				switch ( $mollie_payment->get_method() ) {
777
					case Methods::BELFIUS:
778
					case Methods::DIRECT_DEBIT:
779
					case Methods::IDEAL:
780
					case Methods::KBC:
781
					case Methods::SOFORT:
782
						$consumer_bank_details->set_iban( $mollie_payment_details->consumerAccount );
783
784
						break;
785
					case Methods::BANCONTACT:
786
					case Methods::BANKTRANSFER:
787
					case Methods::PAYPAL:
788
					default:
789
						$consumer_bank_details->set_account_number( $mollie_payment_details->consumerAccount );
790
791
						break;
792
				}
793
			}
794
795
			if ( isset( $mollie_payment_details->consumerBic ) ) {
796
				$consumer_bank_details->set_bic( $mollie_payment_details->consumerBic );
797
			}
798
799
			/*
800
			 * Failure reason.
801
			 */
802
			$failure_reason = $payment->get_failure_reason();
803
804
			if ( null === $failure_reason ) {
805
				$failure_reason = new FailureReason();
806
			}
807
808
			// SEPA Direct Debit.
809
			if ( isset( $mollie_payment_details->bankReasonCode ) ) {
810
				$failure_reason->set_code( $mollie_payment_details->bankReasonCode );
811
			}
812
813
			if ( isset( $mollie_payment_details->bankReason ) ) {
814
				$failure_reason->set_message( $mollie_payment_details->bankReason );
815
			}
816
817
			// Credit card.
818
			if ( isset( $mollie_payment_details->failureReason ) ) {
819
				$failure_reason->set_code( $mollie_payment_details->failureReason );
820
			}
821
822
			if ( isset( $mollie_payment_details->failureMessage ) ) {
823
				$failure_reason->set_message( $mollie_payment_details->failureMessage );
824
			}
825
826
			$failure_code    = $failure_reason->get_code();
827
			$failure_message = $failure_reason->get_message();
828
829
			if ( ! empty( $failure_code ) || ! empty( $failure_message ) ) {
830
				$payment->set_failure_reason( $failure_reason );
831
			}
832
		}
833
834
		$links = $mollie_payment->get_links();
835
836
		// Change payment state URL.
837
		if ( \property_exists( $links, 'changePaymentState' ) ) {
838
			$payment->set_meta( 'mollie_change_payment_state_url', $links->changePaymentState->href );
839
		}
840
841
		// phpcs:enable WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase -- Mollie JSON object.
842
843
		if ( $mollie_payment->has_chargebacks() ) {
844
			$mollie_chargebacks = $this->client->get_payment_chargebacks(
845
				$mollie_payment->get_id(),
846
				array( 'limit' => 1 )
847
			);
848
849
			$mollie_chargeback = \reset( $mollie_chargebacks );
850
851
			if ( false !== $mollie_chargeback ) {
852
				$subscriptions = array_filter(
853
					$payment->get_subscriptions(),
854
					function ( $subscription ) {
855
						return SubscriptionStatus::ACTIVE === $subscription->get_status();
856
					}
857
				);
858
859
				foreach ( $subscriptions as $subscription ) {
860
					if ( $mollie_chargeback->get_created_at() > $subscription->get_activated_at() ) {
861
						$subscription->set_status( SubscriptionStatus::ON_HOLD );
862
863
						$subscription->add_note(
864
							\sprintf(
865
								/* translators: 1: Mollie chargeback ID, 2: Mollie payment ID */
866
								\__( 'Subscription put on hold due to chargeback `%1$s` of payment `%2$s`.', 'pronamic_ideal' ),
867
								\esc_html( $mollie_chargeback->get_id() ),
868
								\esc_html( $mollie_payment->get_id() )
869
							)
870
						);
871
					}
872
				}
873
			}
874
		}
875
876
		// Refunds.
877
		$amount_refunded = $mollie_payment->get_amount_refunded();
878
879
		if ( null !== $amount_refunded ) {
880
			$refunded_amount = new Money( $amount_refunded->get_value(), $amount_refunded->get_currency() );
881 10
882 10
			$payment->set_refunded_amount( $refunded_amount->get_value() > 0 ? $refunded_amount : null );
883
		}
884 10
885
		// Save.
886 10
		$payment->save();
887
888
		foreach ( $payment->get_subscriptions() as $subscription ) {
889
			$subscription->save();
890
		}
891
	}
892
893
	/**
894
	 * Update subscription mandate.
895 10
	 *
896 10
	 * @param Subscription $subscription   Subscription.
897
	 * @param string       $mandate_id     Mollie mandate ID.
898
	 * @param string|null  $payment_method Payment method.
899 10
	 * @return void
900
	 * @throws \Exception Throws exception if subscription note could not be added.
901 10
	 */
902 10
	public function update_subscription_mandate( Subscription $subscription, $mandate_id, $payment_method = null ) {
903
		$customer_id = (string) $subscription->get_meta( 'mollie_customer_id' );
904 10
905 4
		$mandate = $this->client->get_mandate( $mandate_id, $customer_id );
906
907
		if ( ! \is_object( $mandate ) ) {
908
			return;
909
		}
910 10
911
		// Update mandate.
912 10
		$old_mandate_id = $subscription->get_meta( 'mollie_mandate_id' );
913 10
914
		$subscription->set_meta( 'mollie_mandate_id', $mandate_id );
915 10
916 7
		if ( ! empty( $old_mandate_id ) && $old_mandate_id !== $mandate_id ) {
917
			$note = \sprintf(
918 7
			/* translators: 1: old mandate ID, 2: new mandate ID */
919
				\__( 'Mandate for subscription changed from "%1$s" to "%2$s".', 'pronamic_ideal' ),
920
				\esc_html( $old_mandate_id ),
921
				\esc_html( $mandate_id )
922 10
			);
923
924
			$subscription->add_note( $note );
925
		}
926
927
		// Update payment method.
928
		$old_method = $subscription->get_payment_method();
929
		$new_method = ( null === $payment_method && \property_exists( $mandate, 'method' ) ? Methods::transform_gateway_method( $mandate->method ) : $payment_method );
930
931 24
		// `Direct Debit` is not a recurring method, use `Direct Debit (mandate via ...)` instead.
932 24
		if ( PaymentMethods::DIRECT_DEBIT === $new_method ) {
933
			$new_method = PaymentMethods::DIRECT_DEBIT_IDEAL;
934 24
935
			// Use `Direct Debit (mandate via Bancontact)` if consumer account starts with `BE`.
936
			if ( \property_exists( $mandate, 'details' ) && 'BE' === \substr( $mandate->details->consumerAccount, 0, 2 ) ) {
937
				$new_method = PaymentMethods::DIRECT_DEBIT_BANCONTACT;
938 24
			}
939
		}
940 24
941
		if ( ! empty( $old_method ) && $old_method !== $new_method ) {
942 24
			$subscription->set_payment_method( $new_method );
943
944
			// Add note.
945
			$note = \sprintf(
946
				/* translators: 1: old payment method, 2: new payment method */
947
				\__( 'Payment method for subscription changed from "%1$s" to "%2$s".', 'pronamic_ideal' ),
948
				\esc_html( (string) PaymentMethods::get_name( $old_method ) ),
949
				\esc_html( (string) PaymentMethods::get_name( $new_method ) )
950
			);
951 10
952 10
			$subscription->add_note( $note );
953
		}
954 10
955
		$subscription->save();
956 7
	}
957
958 7
	/**
959 7
	 * Create refund.
960
	 *
961
	 * @param string $transaction_id Transaction ID.
962
	 * @param Money  $amount         Amount to refund.
963 10
	 * @param string $description    Refund reason.
964 6
	 * @return string
965
	 */
966
	public function create_refund( $transaction_id, Money $amount, $description = null ) {
967 4
		$request = new RefundRequest( AmountTransformer::transform( $amount ) );
968
969
		// Metadata payment ID.
970
		$payment = \get_pronamic_payment_by_transaction_id( $transaction_id );
971
972
		if ( null !== $payment ) {
973
			$request->set_metadata(
974
				array(
975
					'pronamic_payment_id' => $payment->get_id(),
976
				)
977 10
			);
978 10
		}
979
980 10
		// Description.
981
		if ( ! empty( $description ) ) {
982 10
			$request->set_description( $description );
983
		}
984 4
985
		$refund = $this->client->create_refund( $transaction_id, $request );
986
987
		return $refund->get_id();
988
	}
989
990
	/**
991
	 * Get Mollie customer ID for payment.
992
	 *
993
	 * @param Payment $payment Payment.
994 4
	 * @return string|null
995 4
	 */
996
	public function get_customer_id_for_payment( Payment $payment ) {
997
		$customer_ids = $this->get_customer_ids_for_payment( $payment );
998
999 6
		$customer_id = $this->get_first_existing_customer_id( $customer_ids );
1000
1001
		return $customer_id;
1002
	}
1003
1004
	/**
1005
	 * Get Mollie customers for the specified payment.
1006
	 *
1007
	 * @param Payment $payment Payment.
1008
	 * @return array<string>
1009
	 */
1010
	private function get_customer_ids_for_payment( Payment $payment ) {
1011
		$customer_ids = array();
1012
1013
		// Customer ID from subscription meta.
1014
		$subscription = $payment->get_subscription();
1015
1016
		if ( null !== $subscription ) {
1017
			$customer_id = $this->get_customer_id_for_subscription( $subscription );
1018
1019
			if ( null !== $customer_id ) {
1020
				$customer_ids[] = $customer_id;
1021
			}
1022
		}
1023
1024
		// Customer ID from WordPress user.
1025
		$customer = $payment->get_customer();
1026
1027
		if ( null !== $customer ) {
1028
			$user_id = $customer->get_user_id();
1029
1030
			if ( ! empty( $user_id ) ) {
1031
				$user_customer_ids = $this->get_customer_ids_for_user( $user_id );
1032
1033
				$customer_ids = \array_merge( $customer_ids, $user_customer_ids );
1034
			}
1035
		}
1036
1037
		return $customer_ids;
1038
	}
1039
1040
	/**
1041
	 * Get Mollie customers for the specified WordPress user ID.
1042
	 *
1043
	 * @param int $user_id WordPress user ID.
1044
	 * @return array<string>
1045
	 */
1046
	public function get_customer_ids_for_user( $user_id ) {
1047
		$customer_query = new CustomerQuery(
1048
			array(
1049
				'user_id' => $user_id,
1050
			)
1051
		);
1052
1053
		$customers = $customer_query->get_customers();
1054
1055
		$customer_ids = wp_list_pluck( $customers, 'mollie_id' );
1056
1057
		return $customer_ids;
1058
	}
1059
1060
	/**
1061
	 * Get customer ID for subscription.
1062
	 *
1063
	 * @param Subscription $subscription Subscription.
1064
	 * @return string|null
1065
	 */
1066
	private function get_customer_id_for_subscription( Subscription $subscription ) {
1067
		$customer_id = $subscription->get_meta( 'mollie_customer_id' );
1068
1069
		if ( empty( $customer_id ) ) {
1070
			// Try to get (legacy) customer ID from first payment.
1071
			$first_payment = $subscription->get_first_payment();
1072
1073
			if ( null !== $first_payment ) {
1074
				$customer_id = $first_payment->get_meta( 'mollie_customer_id' );
1075
			}
1076
		}
1077
1078 27
		if ( empty( $customer_id ) ) {
1079 27
			return null;
1080 1
		}
1081
1082
		return $customer_id;
1083
	}
1084 26
1085
	/**
1086
	 * Get first existing customer from customers list.
1087 26
	 *
1088
	 * @param array<string> $customer_ids Customers.
1089 26
	 * @return string|null
1090 16
	 * @throws Error Throws error on Mollie error.
1091
	 */
1092
	private function get_first_existing_customer_id( $customer_ids ) {
1093 26
		$customer_ids = \array_filter( $customer_ids );
1094
1095
		$customer_ids = \array_unique( $customer_ids );
1096
1097
		foreach ( $customer_ids as $customer_id ) {
1098 26
			try {
1099
				$customer = $this->client->get_customer( $customer_id );
1100 26
			} catch ( Error $error ) {
1101 3
				// Check for status 410 ("Gone - The customer is no longer available").
1102
				if ( 410 === $error->get_status() ) {
1103
					continue;
1104 23
				}
1105
1106 23
				throw $error;
1107 12
			}
1108
1109
			if ( null !== $customer ) {
1110
				return $customer_id;
1111 11
			}
1112
		}
1113
1114 11
		return null;
1115
	}
1116
1117 11
	/**
1118 11
	 * Create customer for payment.
1119
	 *
1120
	 * @param Payment $payment Payment.
1121
	 * @return string|null
1122 11
	 * @throws Error Throws Error when Mollie error occurs.
1123 11
	 * @throws \Exception Throws exception when error in customer data store occurs.
1124
	 */
1125 11
	private function create_customer_for_payment( Payment $payment ) {
1126 2
		$mollie_customer = new Customer();
1127
		$mollie_customer->set_mode( $this->config->is_test_mode() ? 'test' : 'live' );
1128 2
		$mollie_customer->set_email( $payment->get_email() );
1129
1130 2
		$pronamic_customer = $payment->get_customer();
1131
1132 11
		if ( null !== $pronamic_customer ) {
1133
			// Name.
1134
			$name = (string) $pronamic_customer->get_name();
1135
1136
			if ( '' !== $name ) {
1137
				$mollie_customer->set_name( $name );
1138
			}
1139
1140
			// Locale.
1141
			$locale = $pronamic_customer->get_locale();
1142
1143
			if ( null !== $locale ) {
1144
				$mollie_customer->set_locale( LocaleHelper::transform( $locale ) );
1145
			}
1146
		}
1147
1148
		// Try to get name from consumer bank details.
1149
		$consumer_bank_details = $payment->get_consumer_bank_details();
1150
1151
		if ( null === $mollie_customer->get_name() && null !== $consumer_bank_details ) {
1152
			$name = $consumer_bank_details->get_name();
1153
1154
			if ( null !== $name ) {
1155
				$mollie_customer->set_name( $name );
1156
			}
1157
		}
1158
1159
		// Create customer.
1160
		$mollie_customer = $this->client->create_customer( $mollie_customer );
1161
1162
		$customer_id = $this->customer_data_store->insert_customer( $mollie_customer );
1163
1164
		// Connect to user.
1165
		if ( null !== $pronamic_customer ) {
1166
			$user_id = $pronamic_customer->get_user_id();
1167
1168
			if ( null !== $user_id ) {
1169
				$user = \get_user_by( 'id', $user_id );
1170
1171
				if ( false !== $user ) {
1172
					$this->customer_data_store->connect_mollie_customer_to_wp_user( $mollie_customer, $user );
1173
				}
1174
			}
1175
		}
1176
1177
		// Store customer ID in subscription meta.
1178
		$subscription = $payment->get_subscription();
1179
1180
		if ( null !== $subscription ) {
1181
			$subscription->set_meta( 'mollie_customer_id', $mollie_customer->get_id() );
1182
		}
1183
1184
		return $mollie_customer->get_id();
1185
	}
1186
1187
	/**
1188
	 * Copy Mollie customer ID from subscription meta to WordPress user meta.
1189
	 *
1190
	 * @param Payment $payment Payment.
1191
	 * @return void
1192
	 */
1193
	public function copy_customer_id_to_wp_user( Payment $payment ) {
1194
		if ( $this->config->id !== $payment->config_id ) {
1195
			return;
1196
		}
1197
1198
		// Subscription.
1199
		$subscription = $payment->get_subscription();
1200
1201
		// Customer.
1202
		$customer = $payment->get_customer();
1203
1204
		if ( null === $customer && null !== $subscription ) {
1205
			$customer = $subscription->get_customer();
1206
		}
1207
1208
		if ( null === $customer ) {
1209
			return;
1210
		}
1211
1212
		// WordPress user.
1213
		$user_id = $customer->get_user_id();
1214
1215
		if ( null === $user_id ) {
1216
			return;
1217
		}
1218
1219
		$user = \get_user_by( 'id', $user_id );
1220
1221
		if ( false === $user ) {
1222
			return;
1223
		}
1224
1225
		// Customer IDs.
1226
		$customer_ids = array();
1227
1228
		// Payment.
1229
		$customer_ids[] = $payment->get_meta( 'mollie_customer_id' );
1230
1231
		// Subscription.
1232
		if ( null !== $subscription ) {
1233
			$customer_ids[] = $subscription->get_meta( 'mollie_customer_id' );
1234
		}
1235
1236
		// Connect.
1237
		$customer_ids = \array_filter( $customer_ids );
1238
		$customer_ids = \array_unique( $customer_ids );
1239
1240
		foreach ( $customer_ids as $customer_id ) {
1241
			$customer = new Customer( $customer_id );
1242
1243
			$this->customer_data_store->get_or_insert_customer( $customer );
1244
1245
			$this->customer_data_store->connect_mollie_customer_to_wp_user( $customer, $user );
1246
		}
1247
	}
1248
}
1249