Failed Conditions
Push — develop ( 82d995...ef1d84 )
by Remco
07:41 queued 39s
created

src/Gateway.php (4 issues)

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

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

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

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

// Bar.php
namespace OtherDir;

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

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

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

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

// Bar.php
namespace OtherDir;

use SomeDir\Foo as SomeDirFoo; // There is no conflict anymore.
Loading history...
21
use Pronamic\WordPress\Pay\Payments\PaymentStatus;
22
use Pronamic\WordPress\Pay\Subscriptions\Subscription;
23
use Pronamic\WordPress\Pay\Subscriptions\SubscriptionStatus;
24
25
/**
26
 * Title: Mollie
27
 * Description:
28
 * Copyright: 2005-2021 Pronamic
29
 * Company: Pronamic
30
 *
31
 * @author  Remco Tolsma
32
 * @version 2.1.4
33
 * @since   1.1.0
34
 */
35
class Gateway extends Core_Gateway {
36
	/**
37
	 * Client.
38
	 *
39
	 * @var Client
40
	 */
41
	protected $client;
42
43
	/**
44
	 * Config
45
	 *
46
	 * @var Config
47
	 */
48
	protected $config;
49
50
	/**
51
	 * Profile data store.
52
	 *
53
	 * @var ProfileDataStore
54
	 */
55
	private $profile_data_store;
56
57
	/**
58
	 * Customer data store.
59
	 *
60
	 * @var CustomerDataStore
61
	 */
62
	private $customer_data_store;
63
64
	/**
65
	 * Constructs and initializes an Mollie gateway
66
	 *
67
	 * @param Config $config Config.
68
	 */
69 39
	public function __construct( Config $config ) {
70 39
		parent::__construct( $config );
71
72 39
		$this->set_method( self::METHOD_HTTP_REDIRECT );
73
74
		// Supported features.
75 39
		$this->supports = array(
76
			'payment_status_request',
77
			'recurring_direct_debit',
78
			'recurring_credit_card',
79
			'recurring',
80
			'refunds',
81
			'webhook',
82
			'webhook_log',
83
			'webhook_no_config',
84
		);
85
86
		// Client.
87 39
		$this->client = new Client( (string) $config->api_key );
88
89
		// Data Stores.
90 39
		$this->profile_data_store  = new ProfileDataStore();
91 39
		$this->customer_data_store = new CustomerDataStore();
92
93
		// Actions.
94 39
		add_action( 'pronamic_payment_status_update', array( $this, 'copy_customer_id_to_wp_user' ), 99, 1 );
95 39
	}
96
97
	/**
98
	 * Get issuers
99
	 *
100
	 * @see Core_Gateway::get_issuers()
101
	 * @return array<int, array<string, array<string>>>
102
	 */
103 3
	public function get_issuers() {
104 3
		$groups = array();
105
106
		try {
107 3
			$result = $this->client->get_issuers();
108
109
			$groups[] = array(
110
				'options' => $result,
111
			);
112 3
		} catch ( Error $e ) {
113
			// Catch Mollie error.
114 3
			$error = new \WP_Error(
115 3
				'mollie_error',
116 3
				sprintf( '%1$s (%2$s) - %3$s', $e->get_title(), $e->getCode(), $e->get_detail() )
117
			);
118
119 3
			$this->set_error( $error );
120
		} catch ( \Exception $e ) {
121
			// Catch exceptions.
122
			$error = new \WP_Error( 'mollie_error', $e->getMessage() );
123
124
			$this->set_error( $error );
125
		}
126
127 3
		return $groups;
128
	}
129
130
	/**
131
	 * Get available payment methods.
132
	 *
133
	 * @see Core_Gateway::get_available_payment_methods()
134
	 * @return array<int, string>
135
	 */
136 2
	public function get_available_payment_methods() {
137 2
		$payment_methods = array();
138
139
		// Set sequence types to get payment methods for.
140 2
		$sequence_types = array( Sequence::ONE_OFF, Sequence::RECURRING, Sequence::FIRST );
141
142 2
		$results = array();
143
144 2
		foreach ( $sequence_types as $sequence_type ) {
145
			// Get active payment methods for Mollie account.
146
			try {
147 2
				$result = $this->client->get_payment_methods( $sequence_type );
148 2
			} catch ( Error $e ) {
149
				// Catch Mollie error.
150
				$error = new \WP_Error(
151
					'mollie_error',
152
					sprintf( '%1$s (%2$s) - %3$s', $e->get_title(), $e->getCode(), $e->get_detail() )
153
				);
154
155
				$this->set_error( $error );
156
157
				break;
158 2
			} catch ( \Exception $e ) {
159
				// Catch exceptions.
160 2
				$error = new \WP_Error( 'mollie_error', $e->getMessage() );
161
162 2
				$this->set_error( $error );
163
164 2
				break;
165
			}
166
167 2
			if ( Sequence::FIRST === $sequence_type ) {
168
				foreach ( $result as $method => $title ) {
169
					unset( $result[ $method ] );
170
171
					// Get WordPress payment method for direct debit method.
172
					$method         = Methods::transform_gateway_method( $method );
173
					$payment_method = array_search( $method, PaymentMethods::get_recurring_methods(), true );
174
175
					if ( $payment_method ) {
176
						$results[ $payment_method ] = $title;
177
					}
178
				}
179
			}
180
181 2
			if ( is_array( $result ) ) {
182 2
				$results = array_merge( $results, $result );
183
			}
184
		}
185
186
		// Transform to WordPress payment methods.
187 2
		foreach ( $results as $method => $title ) {
188 2
			$method = (string) $method;
189
190 2
			$payment_method = Methods::transform_gateway_method( $method );
191
192 2
			if ( PaymentMethods::is_recurring_method( $method ) ) {
193
				$payment_method = $method;
194
			}
195
196 2
			if ( null !== $payment_method ) {
197 2
				$payment_methods[] = (string) $payment_method;
198
			}
199
		}
200
201 2
		$payment_methods = array_unique( $payment_methods );
202
203 2
		return $payment_methods;
204
	}
205
206
	/**
207
	 * Get supported payment methods
208
	 *
209
	 * @see Core_Gateway::get_supported_payment_methods()
210
	 * @return array<string>
211
	 */
212 2
	public function get_supported_payment_methods() {
213
		return array(
214 2
			PaymentMethods::APPLE_PAY,
215
			PaymentMethods::BANCONTACT,
216
			PaymentMethods::BANK_TRANSFER,
217
			PaymentMethods::BELFIUS,
218
			PaymentMethods::CREDIT_CARD,
219
			PaymentMethods::DIRECT_DEBIT,
220
			PaymentMethods::DIRECT_DEBIT_BANCONTACT,
221
			PaymentMethods::DIRECT_DEBIT_IDEAL,
222
			PaymentMethods::DIRECT_DEBIT_SOFORT,
223
			PaymentMethods::EPS,
224
			PaymentMethods::GIROPAY,
225
			PaymentMethods::IDEAL,
226
			PaymentMethods::KBC,
227
			PaymentMethods::PAYPAL,
228
			PaymentMethods::PRZELEWY24,
229
			PaymentMethods::SOFORT,
230
		);
231
	}
232
233
	/**
234
	 * Get webhook URL for Mollie.
235
	 *
236
	 * @return string|null
237
	 */
238 4
	public function get_webhook_url() {
239 4
		$url = \rest_url( Integration::REST_ROUTE_NAMESPACE . '/webhook' );
240
241 4
		$host = wp_parse_url( $url, PHP_URL_HOST );
242
243 4
		if ( is_array( $host ) ) {
244
			// Parsing failure.
245
			$host = '';
246
		}
247
248 4
		if ( 'localhost' === $host ) {
249
			// Mollie doesn't allow localhost.
250 1
			return null;
251 3
		} elseif ( '.dev' === substr( $host, -4 ) ) {
252
			// Mollie doesn't allow the .dev TLD.
253 1
			return null;
254 2
		} elseif ( '.local' === substr( $host, -6 ) ) {
255
			// Mollie doesn't allow the .local TLD.
256 1
			return null;
257 1
		} elseif ( '.test' === substr( $host, -5 ) ) {
258
			// Mollie doesn't allow the .test TLD.
259
			return null;
260
		}
261
262 1
		return $url;
263
	}
264
265
	/**
266
	 * Start
267
	 *
268
	 * @see Core_Gateway::start()
269
	 * @param Payment $payment Payment.
270
	 * @return void
271
	 * @throws \Exception Throws exception on error creating Mollie customer for payment.
272
	 */
273
	public function start( Payment $payment ) {
274
		$description = (string) $payment->get_description();
275
276
		/**
277
		 * Filters the Mollie payment description.
278
		 * 
279
		 * The maximum length of the description field differs per payment
280
		 * method, with the absolute maximum being 255 characters.
281
		 *
282
		 * @link https://docs.mollie.com/reference/v2/payments-api/create-payment#parameters
283
		 * @since 3.0.1
284
		 * @param string  $description Description.
285
		 * @param Payment $payment     Payment.
286
		 */
287
		$description = \apply_filters( 'pronamic_pay_mollie_payment_description', $description, $payment );
288
289
		$request = new PaymentRequest(
290
			AmountTransformer::transform( $payment->get_total_amount() ),
291
			$description
292
		);
293
294
		$request->redirect_url = $payment->get_return_url();
295
		$request->webhook_url  = $this->get_webhook_url();
296
297
		// Locale.
298
		$customer = $payment->get_customer();
299
300
		if ( null !== $customer ) {
301
			$request->locale = LocaleHelper::transform( $customer->get_locale() );
302
		}
303
304
		// Customer ID.
305
		$customer_id = $this->get_customer_id_for_payment( $payment );
306
307
		if ( null === $customer_id ) {
308
			$customer_id = $this->create_customer_for_payment( $payment );
309
		}
310
311
		if ( null !== $customer_id ) {
312
			$request->customer_id = $customer_id;
313
		}
314
315
		// Payment method.
316
		$payment_method = $payment->get_method();
317
318
		// Recurring payment method.
319
		$subscription = $payment->get_subscription();
320
321
		$is_recurring_method = ( $subscription && PaymentMethods::is_recurring_method( (string) $payment_method ) );
322
323
		// Consumer bank details.
324
		$consumer_bank_details = $payment->get_consumer_bank_details();
325
326
		if ( PaymentMethods::DIRECT_DEBIT === $payment_method && null !== $consumer_bank_details ) {
327
			$consumer_name = $consumer_bank_details->get_name();
328
			$consumer_iban = $consumer_bank_details->get_iban();
329
330
			$request->consumer_name    = $consumer_name;
331
			$request->consumer_account = $consumer_iban;
332
333
			// Check if one-off SEPA Direct Debit can be used, otherwise short circuit payment.
334
			if ( null !== $customer_id ) {
335
				// Find or create mandate.
336
				$mandate_id = $this->client->has_valid_mandate( $customer_id, PaymentMethods::DIRECT_DEBIT, $consumer_iban );
337
338
				if ( false === $mandate_id ) {
339
					$mandate = $this->client->create_mandate( $customer_id, $consumer_bank_details );
340
341
					if ( ! \property_exists( $mandate, 'id' ) ) {
342
						throw new \Exception( 'Missing mandate ID.' );
343
					}
344
345
					$mandate_id = $mandate->id;
346
				}
347
348
				// Charge immediately on-demand.
349
				$request->set_sequence_type( Sequence::RECURRING );
350
				$request->set_mandate_id( (string) $mandate_id );
351
352
				$is_recurring_method = true;
353
354
				$payment->recurring = true;
355
			}
356
		}
357
358
		if ( false === $is_recurring_method && null !== $payment_method ) {
359
			// Always use 'direct debit mandate via iDEAL/Bancontact/Sofort' payment methods as recurring method.
360
			$is_recurring_method = PaymentMethods::is_direct_debit_method( $payment_method );
361
362
			// Check for non-recurring methods for subscription payments.
363
			if ( false === $is_recurring_method && null !== $payment->get_periods() ) {
364
				$direct_debit_methods = PaymentMethods::get_direct_debit_methods();
365
366
				$is_recurring_method = \in_array( $payment_method, $direct_debit_methods, true );
367
			}
368
		}
369
370
		if ( $is_recurring_method ) {
371
			$request->sequence_type = $payment->get_recurring() ? Sequence::RECURRING : Sequence::FIRST;
372
373
			if ( Sequence::FIRST === $request->sequence_type ) {
374
				$payment_method = PaymentMethods::get_first_payment_method( $payment_method );
375
			}
376
377
			if ( Sequence::RECURRING === $request->sequence_type ) {
378
				// Use mandate from subscription.
379
				if ( $subscription && empty( $request->mandate_id ) ) {
380
					$subscription_mandate_id = $subscription->get_meta( 'mollie_mandate_id' );
381
382
					if ( false !== $subscription_mandate_id ) {
383
						$request->set_mandate_id( $subscription_mandate_id );
384
					}
385
				}
386
387
				// Use credit card for recurring Apple Pay payments.
388
				if ( PaymentMethods::APPLE_PAY === $payment_method ) {
389
					$payment_method = PaymentMethods::CREDIT_CARD;
390
				}
391
392
				$direct_debit_methods = PaymentMethods::get_direct_debit_methods();
393
394
				$recurring_method = \array_search( $payment_method, $direct_debit_methods, true );
395
396
				if ( \is_string( $recurring_method ) ) {
397
					$payment_method = $recurring_method;
398
				}
399
400
				$payment->set_action_url( $payment->get_return_url() );
401
			}
402
		}
403
404
		// Leap of faith if the WordPress payment method could not transform to a Mollie method?
405
		$request->method = Methods::transform( $payment_method, $payment_method );
406
407
		/**
408
		 * Metadata.
409
		 *
410
		 * Provide any data you like, for example a string or a JSON object.
411
		 * We will save the data alongside the payment. Whenever you fetch
412
		 * the payment with our API, we’ll also include the metadata. You
413
		 * can use up to approximately 1kB.
414
		 *
415
		 * @link https://docs.mollie.com/reference/v2/payments-api/create-payment
416
		 * @link https://en.wikipedia.org/wiki/Metadata
417
		 */
418
		$metadata = null;
419
420
		/**
421
		 * Filters the Mollie metadata.
422
		 *
423
		 * @since 2.2.0
424
		 *
425
		 * @param mixed   $metadata Metadata.
426
		 * @param Payment $payment  Payment.
427
		 */
428
		$metadata = \apply_filters( 'pronamic_pay_mollie_payment_metadata', $metadata, $payment );
429
430
		$request->set_metadata( $metadata );
431
432
		// Issuer.
433
		if ( Methods::IDEAL === $request->method ) {
434
			$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...
435
		}
436
437
		// Billing email.
438
		$billing_email = $payment->get_email();
439
440
		/**
441
		 * Filters the Mollie payment billing email used for bank transfer payment instructions.
442
		 *
443
		 * @since 2.2.0
444
		 *
445
		 * @param string|null $billing_email Billing email.
446
		 * @param Payment     $payment       Payment.
447
		 */
448
		$billing_email = \apply_filters( 'pronamic_pay_mollie_payment_billing_email', $billing_email, $payment );
449
450
		$request->set_billing_email( $billing_email );
451
452
		// Due date.
453
		if ( ! empty( $this->config->due_date_days ) ) {
454
			try {
455
				$due_date = new DateTime( sprintf( '+%s days', $this->config->due_date_days ) );
456
			} catch ( \Exception $e ) {
457
				$due_date = null;
458
			}
459
460
			$request->set_due_date( $due_date );
461
		}
462
463
		// Create payment.
464
		$result = $this->client->create_payment( $request );
465
466
		// phpcs:disable WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase -- Mollie JSON object.
467
468
		// Set transaction ID.
469
		if ( isset( $result->id ) ) {
470
			$payment->set_transaction_id( $result->id );
471
		}
472
473
		// Set expiry date.
474
		if ( isset( $result->expiresAt ) ) {
475
			try {
476
				$expires_at = new DateTime( $result->expiresAt );
477
			} catch ( \Exception $e ) {
478
				$expires_at = null;
479
			}
480
481
			$payment->set_expiry_date( $expires_at );
482
		}
483
484
		// Set status.
485
		if ( isset( $result->status ) ) {
486
			$payment->set_status( Statuses::transform( $result->status ) );
487
		}
488
489
		// Set bank transfer recipient details.
490
		if ( isset( $result->details ) ) {
491
			$bank_transfer_recipient_details = $payment->get_bank_transfer_recipient_details();
492
493
			if ( null === $bank_transfer_recipient_details ) {
494
				$bank_transfer_recipient_details = new BankTransferDetails();
495
496
				$payment->set_bank_transfer_recipient_details( $bank_transfer_recipient_details );
497
			}
498
499
			$bank_details = $bank_transfer_recipient_details->get_bank_account();
500
501
			if ( null === $bank_details ) {
502
				$bank_details = new BankAccountDetails();
503
504
				$bank_transfer_recipient_details->set_bank_account( $bank_details );
505
			}
506
507
			$details = $result->details;
508
509
			if ( isset( $details->bankName ) ) {
510
				/**
511
				 * Set `bankName` as bank details name, as result "Stichting Mollie Payments"
512
				 * is not the name of a bank, but the account holder name.
513
				 */
514
				$bank_details->set_name( $details->bankName );
515
			}
516
517
			if ( isset( $details->bankAccount ) ) {
518
				$bank_details->set_iban( $details->bankAccount );
519
			}
520
521
			if ( isset( $details->bankBic ) ) {
522
				$bank_details->set_bic( $details->bankBic );
523
			}
524
525
			if ( isset( $details->transferReference ) ) {
526
				$bank_transfer_recipient_details->set_reference( $details->transferReference );
527
			}
528
		}
529
530
		// Handle links.
531
		if ( isset( $result->_links ) ) {
532
			$links = $result->_links;
533
534
			// Action URL.
535
			if ( isset( $links->checkout->href ) ) {
536
				$payment->set_action_url( $links->checkout->href );
537
			}
538
539
			// Change payment state URL.
540
			if ( isset( $links->changePaymentState->href ) ) {
541
				$payment->set_meta( 'mollie_change_payment_state_url', $links->changePaymentState->href );
542
			}
543
		}
544
545
		// phpcs:enable WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase -- Mollie JSON object.
546
	}
547
548
	/**
549
	 * Update status of the specified payment
550
	 *
551
	 * @param Payment $payment Payment.
552
	 * @return void
553
	 */
554
	public function update_status( Payment $payment ) {
555
		$transaction_id = $payment->get_transaction_id();
556
557
		if ( null === $transaction_id ) {
558
			return;
559
		}
560
561
		$mollie_payment = $this->client->get_payment( $transaction_id );
562
563
		$payment->set_status( Statuses::transform( $mollie_payment->get_status() ) );
564
565
		/**
566
		 * Mollie profile.
567
		 */
568
		$mollie_profile = new Profile();
569
570
		$mollie_profile->set_id( $mollie_payment->get_profile_id() );
571
572
		$profile_internal_id = $this->profile_data_store->get_or_insert_profile( $mollie_profile );
573
574
		/**
575
		 * If the Mollie payment contains a customer ID we will try to connect
576
		 * this Mollie customer ID the WordPress user and subscription.
577
		 * This can be useful in case when a WordPress user is created after
578
		 * a successful payment.
579
		 *
580
		 * @link https://www.gravityforms.com/add-ons/user-registration/
581
		 */
582
		$mollie_customer_id = $mollie_payment->get_customer_id();
583
584
		if ( null !== $mollie_customer_id ) {
585
			$mollie_customer = new Customer( $mollie_customer_id );
586
587
			$customer_internal_id = $this->customer_data_store->get_or_insert_customer(
588
				$mollie_customer,
589
				array(
590
					'profile_id' => $profile_internal_id,
591
				),
592
				array(
593
					'profile_id' => '%s',
594
				)
595
			);
596
597
			// Meta.
598
			$customer_id = $payment->get_meta( 'mollie_customer_id' );
599
600
			if ( empty( $customer_id ) ) {
601
				$payment->set_meta( 'mollie_customer_id', $mollie_customer->get_id() );
602
			}
603
604
			// Customer.
605
			$customer = $payment->get_customer();
606
607
			if ( null !== $customer ) {
608
				// Connect to user.
609
				$user_id = $customer->get_user_id();
610
611
				if ( null !== $user_id ) {
612
					$user = \get_user_by( 'id', $user_id );
613
614
					if ( false !== $user ) {
615
						$this->customer_data_store->connect_mollie_customer_to_wp_user( $mollie_customer, $user );
616
					}
617
				}
618
			}
619
620
			// Subscription.
621
			$subscription = $payment->get_subscription();
622
623
			if ( null !== $subscription ) {
624
				$customer_id = $subscription->get_meta( 'mollie_customer_id' );
625
626
				if ( empty( $customer_id ) ) {
627
					$subscription->set_meta( 'mollie_customer_id', $mollie_customer->get_id() );
628
				}
629
630
				// Update mandate in subscription meta.
631
				$mollie_mandate_id = $mollie_payment->get_mandate_id();
632
633
				if ( null !== $mollie_mandate_id ) {
634
					$mandate_id = $subscription->get_meta( 'mollie_mandate_id' );
635
636
					// Only update if no mandate has been set yet or if payment succeeded.
637
					if ( empty( $mandate_id ) || PaymentStatus::SUCCESS === $payment->get_status() ) {
638
						$this->update_subscription_mandate( $subscription, $mollie_mandate_id, $payment->get_method() );
639
					}
640
				}
641
			}
642
		}
643
644
		// phpcs:disable WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase -- Mollie JSON object.
645
		$mollie_payment_details = $mollie_payment->get_details();
646
647
		if ( null !== $mollie_payment_details ) {
648
			$consumer_bank_details = $payment->get_consumer_bank_details();
649
650
			if ( null === $consumer_bank_details ) {
651
				$consumer_bank_details = new BankAccountDetails();
652
653
				$payment->set_consumer_bank_details( $consumer_bank_details );
654
			}
655
656
			if ( isset( $mollie_payment_details->consumerName ) ) {
657
				$consumer_bank_details->set_name( $mollie_payment_details->consumerName );
658
			}
659
660
			if ( isset( $mollie_payment_details->cardHolder ) ) {
661
				$consumer_bank_details->set_name( $mollie_payment_details->cardHolder );
662
			}
663
664
			if ( isset( $mollie_payment_details->cardNumber ) ) {
665
				// The last four digits of the card number.
666
				$consumer_bank_details->set_account_number( $mollie_payment_details->cardNumber );
667
			}
668
669
			if ( isset( $mollie_payment_details->cardCountryCode ) ) {
670
				// The ISO 3166-1 alpha-2 country code of the country the card was issued in.
671
				$consumer_bank_details->set_country( $mollie_payment_details->cardCountryCode );
672
			}
673
674
			if ( isset( $mollie_payment_details->consumerAccount ) ) {
675
				switch ( $mollie_payment->get_method() ) {
676
					case Methods::BELFIUS:
677
					case Methods::DIRECT_DEBIT:
678
					case Methods::IDEAL:
679
					case Methods::KBC:
680
					case Methods::SOFORT:
681
						$consumer_bank_details->set_iban( $mollie_payment_details->consumerAccount );
682
683
						break;
684
					case Methods::BANCONTACT:
685
					case Methods::BANKTRANSFER:
686
					case Methods::PAYPAL:
687
					default:
688
						$consumer_bank_details->set_account_number( $mollie_payment_details->consumerAccount );
689
690
						break;
691
				}
692
			}
693
694
			if ( isset( $mollie_payment_details->consumerBic ) ) {
695
				$consumer_bank_details->set_bic( $mollie_payment_details->consumerBic );
696
			}
697
698
			/*
699
			 * Failure reason.
700
			 */
701
			$failure_reason = $payment->get_failure_reason();
702
703
			if ( null === $failure_reason ) {
704
				$failure_reason = new FailureReason();
705
			}
706
707
			// SEPA Direct Debit.
708
			if ( isset( $mollie_payment_details->bankReasonCode ) ) {
709
				$failure_reason->set_code( $mollie_payment_details->bankReasonCode );
710
			}
711
712
			if ( isset( $mollie_payment_details->bankReason ) ) {
713
				$failure_reason->set_message( $mollie_payment_details->bankReason );
714
			}
715
716
			// Credit card.
717
			if ( isset( $mollie_payment_details->failureReason ) ) {
718
				$failure_reason->set_code( $mollie_payment_details->failureReason );
719
			}
720
721
			if ( isset( $mollie_payment_details->failureMessage ) ) {
722
				$failure_reason->set_message( $mollie_payment_details->failureMessage );
723
			}
724
725
			$failure_code    = $failure_reason->get_code();
726
			$failure_message = $failure_reason->get_message();
727
728
			if ( ! empty( $failure_code ) || ! empty( $failure_message ) ) {
729
				$payment->set_failure_reason( $failure_reason );
730
			}
731
		}
732
733
		$links = $mollie_payment->get_links();
734
735
		// Change payment state URL.
736
		if ( \property_exists( $links, 'changePaymentState' ) ) {
737
			$payment->set_meta( 'mollie_change_payment_state_url', $links->changePaymentState->href );
738
		}
739
740
		// phpcs:enable WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase -- Mollie JSON object.
741
742
		if ( $mollie_payment->has_chargebacks() ) {
743
			$mollie_chargebacks = $this->client->get_payment_chargebacks(
744
				$mollie_payment->get_id(),
745
				array( 'limit' => 1 )
746
			);
747
748
			$mollie_chargeback = \reset( $mollie_chargebacks );
749
750
			if ( false !== $mollie_chargeback ) {
751
				$subscriptions = array_filter(
752
					$payment->get_subscriptions(),
753
					function( $subscription ) {
754
						return SubscriptionStatus::ACTIVE === $subscription->get_status();
755
					}
756
				);
757
758
				foreach ( $subscriptions as $subscription ) {
759
					if ( $mollie_chargeback->get_created_at() > $subscription->get_activated_at() ) {
760
						$subscription->set_status( SubscriptionStatus::ON_HOLD );
761
762
						$subscription->add_note(
763
							\sprintf(
764
								/* translators: 1: Mollie chargeback ID, 2: Mollie payment ID */
765
								\__( 'Subscription put on hold due to chargeback `%1$s` of payment `%2$s`.', 'pronamic_ideal' ),
766
								\esc_html( $mollie_chargeback->get_id() ),
767
								\esc_html( $mollie_payment->get_id() )
768
							)
769
						);
770
771
						$subscription->save();
772
					}
773
				}
774
			}
775
		}
776
777
		// Refunds.
778
		$amount_refunded = $mollie_payment->get_amount_refunded();
779
780
		if ( null !== $amount_refunded ) {
781
			$refunded_amount = new Money( $amount_refunded->get_value(), $amount_refunded->get_currency() );
782
783
			$payment->set_refunded_amount( $refunded_amount->get_value() > 0 ? $refunded_amount : null );
784
		}
785
	}
786
787
	/**
788
	 * Update subscription mandate.
789
	 *
790
	 * @param Subscription $subscription   Subscription.
791
	 * @param string       $mandate_id     Mollie mandate ID.
792
	 * @param string|null  $payment_method Payment method.
793
	 * @return void
794
	 * @throws \Exception Throws exception if subscription note could not be added.
795
	 */
796
	public function update_subscription_mandate( Subscription $subscription, $mandate_id, $payment_method = null ) {
797
		$customer_id = (string) $subscription->get_meta( 'mollie_customer_id' );
798
799
		$mandate = $this->client->get_mandate( $mandate_id, $customer_id );
800
801
		if ( ! \is_object( $mandate ) ) {
802
			return;
803
		}
804
805
		// Update mandate.
806
		$old_mandate_id = $subscription->get_meta( 'mollie_mandate_id' );
807
808
		$subscription->set_meta( 'mollie_mandate_id', $mandate_id );
809
810
		if ( ! empty( $old_mandate_id ) && $old_mandate_id !== $mandate_id ) {
811
			$note = \sprintf(
812
			/* translators: 1: old mandate ID, 2: new mandate ID */
813
				\__( 'Mandate for subscription changed from "%1$s" to "%2$s".', 'pronamic_ideal' ),
814
				\esc_html( $old_mandate_id ),
815
				\esc_html( $mandate_id )
816
			);
817
818
			$subscription->add_note( $note );
819
		}
820
821
		// Update payment method.
822
		$old_method = $subscription->get_payment_method();
0 ignored issues
show
The method get_payment_method() does not exist on Pronamic\WordPress\Pay\Subscriptions\Subscription. Did you maybe mean get_payments()? ( Ignorable by Annotation )

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

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

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

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