Gateway::update_status()   A
last analyzed

Complexity

Conditions 2
Paths 2

Size

Total Lines 11
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 6

Importance

Changes 12
Bugs 0 Features 0
Metric Value
cc 2
eloc 5
c 12
b 0
f 0
nc 2
nop 1
dl 0
loc 11
ccs 0
cts 2
cp 0
crap 6
rs 10
1
<?php
2
/**
3
 * Mollie gateway.
4
 *
5
 * @author    Pronamic <[email protected]>
6
 * @copyright 2005-2022 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\Gateways\Mollie\Payment as MolliePayment;
20
use Pronamic\WordPress\Pay\Payments\FailureReason;
21
use Pronamic\WordPress\Pay\Payments\Payment;
0 ignored issues
show
Bug introduced by
This use statement conflicts with another class in this namespace, Pronamic\WordPress\Pay\Gateways\Mollie\Payment. Consider defining an alias.

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

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

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

// Bar.php
namespace OtherDir;

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

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

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

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

// Bar.php
namespace OtherDir;

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