Client   F
last analyzed

Complexity

Total Complexity 60

Size/Duplication

Total Lines 465
Duplicated Lines 0 %

Test Coverage

Coverage 27.52%

Importance

Changes 11
Bugs 0 Features 0
Metric Value
eloc 141
c 11
b 0
f 0
dl 0
loc 465
ccs 41
cts 149
cp 0.2752
rs 3.6
wmc 60

20 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 2 1
A get_current_profile() 0 2 1
A send_request_to_endpoint() 0 2 1
A send_request() 0 35 3
A get_url() 0 4 1
A get_profile() 0 2 1
A get_issuers() 0 19 5
B get_payment_methods() 0 31 7
A create_refund() 0 4 1
A get_payments() 0 2 1
B get_first_valid_mandate_datetime() 0 40 7
A get_payment() 0 10 2
A get_payment_chargebacks() 0 12 4
A create_customer() 0 12 2
A create_payment() 0 6 1
C has_valid_mandate() 0 39 12
A get_mandate() 0 10 3
A get_mandates() 0 6 2
A create_mandate() 0 12 1
A get_customer() 0 13 4

How to fix   Complexity   

Complex Class

Complex classes like Client often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use Client, and based on these observations, apply Extract Interface, too.

1
<?php
2
/**
3
 * Mollie client.
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\Http\Facades\Http;
15
use Pronamic\WordPress\Pay\Banks\BankAccountDetails;
16
use Pronamic\WordPress\Pay\Core\XML\Security;
17
18
/**
19
 * Title: Mollie
20
 * Description:
21
 * Copyright: 2005-2022 Pronamic
22
 * Company: Pronamic
23
 *
24
 * @author  Remco Tolsma
25
 * @version 2.1.4
26
 * @since   1.0.0
27
 */
28
class Client {
29
	/**
30
	 * Mollie API endpoint URL
31
	 *
32
	 * @var string
33
	 */
34
	const API_URL = 'https://api.mollie.com/v2/';
35
36
	/**
37
	 * Mollie API Key ID
38
	 *
39
	 * @var string
40
	 */
41
	private $api_key;
42
43
	/**
44
	 * Constructs and initializes an Mollie client object
45
	 *
46
	 * @param string $api_key Mollie API key.
47
	 */
48 39
	public function __construct( $api_key ) {
49 39
		$this->api_key = $api_key;
50 39
	}
51
52
	/**
53
	 * Send request with the specified action and parameters
54
	 *
55
	 * @param string                            $url    URL.
56
	 * @param string                            $method HTTP method to use.
57
	 * @param array<string, string|object|null> $data   Request data.
58
	 * @return object
59
	 * @throws Error Throws Error when Mollie error occurs.
60
	 * @throws \Exception Throws exception when error occurs.
61
	 */
62 9
	public function send_request( $url, $method = 'GET', array $data = array() ) {
63
		// Request.
64 9
		$response = Http::request(
65 9
			$url,
66
			array(
67 9
				'method'  => $method,
68
				'headers' => array(
69 9
					'Authorization' => 'Bearer ' . $this->api_key,
70
				),
71 9
				'body'    => $data,
72
			)
73
		);
74
75 9
		$data = $response->json();
76
77
		// Object.
78 9
		if ( ! \is_object( $data ) ) {
79
			$code = $response->status();
80
81
			throw new \Exception(
82
				\sprintf( 'Could not JSON decode Mollie response to an object (HTTP Status Code: %s).', $code ),
83
				\intval( $code )
84
			);
85
		}
86
87
		// Mollie error from JSON response.
88 9
		if ( isset( $data->status, $data->title, $data->detail ) ) {
89 3
			throw new Error(
90 3
				$data->status,
91 3
				$data->title,
92 3
				$data->detail
93
			);
94
		}
95
96 6
		return $data;
97
	}
98
99
	/**
100
	 * Get URL.
101
	 *
102
	 * @param string $endpoint URL endpoint.
103
	 * @return string
104
	 */
105 9
	public function get_url( $endpoint ) {
106 9
		$url = self::API_URL . $endpoint;
107
108 9
		return $url;
109
	}
110
111
	/**
112
	 * Send request to endpoint.
113
	 *
114
	 * @param string                            $endpoint Endpoint.
115
	 * @param string                            $method   HTTP method to use.
116
	 * @param array<string, string|object|null> $data     Request data.
117
	 * @return object
118
	 */
119 9
	public function send_request_to_endpoint( $endpoint, $method = 'GET', array $data = array() ) {
120 9
		return $this->send_request( $this->get_url( $endpoint ), $method, $data );
121
	}
122
123
	/**
124
	 * Get profile.
125
	 *
126
	 * @param string $profile Mollie profile ID.
127
	 * @return object
128
	 * @throws Error Throws Error when Mollie error occurs.
129
	 */
130
	public function get_profile( $profile ) {
131
		return $this->send_request_to_endpoint( 'profiles/' . $profile, 'GET' );
132
	}
133
134
	/**
135
	 * Get current profile.
136
	 *
137
	 * @return object
138
	 * @throws Error Throws Error when Mollie error occurs.
139
	 */
140
	public function get_current_profile() {
141
		return $this->get_profile( 'me' );
142
	}
143
144
	/**
145
	 * Create payment.
146
	 *
147
	 * @param PaymentRequest $request Payment request.
148
	 * @return Payment
149
	 */
150
	public function create_payment( PaymentRequest $request ) {
151
		$object = $this->send_request_to_endpoint( 'payments', 'POST', $request->get_array() );
152
153
		$payment = Payment::from_json( $object );
154
155
		return $payment;
156
	}
157
158
	/**
159
	 * Get payments.
160
	 *
161
	 * @return bool|object
162
	 */
163
	public function get_payments() {
164
		return $this->send_request_to_endpoint( 'payments', 'GET' );
165
	}
166
167
	/**
168
	 * Get payment.
169
	 *
170
	 * @param string               $payment_id Mollie payment ID.
171
	 * @param array<string, mixed> $parameters Parameters.
172
	 * @return Payment
173
	 * @throws \InvalidArgumentException Throws exception on empty payment ID argument.
174
	 */
175
	public function get_payment( $payment_id, $parameters = array() ) {
176
		if ( empty( $payment_id ) ) {
177
			throw new \InvalidArgumentException( 'Mollie payment ID can not be empty string.' );
178
		}
179
180
		$object = $this->send_request_to_endpoint( 'payments/' . $payment_id, 'GET', $parameters );
181
182
		$payment = Payment::from_json( $object );
183
184
		return $payment;
185
	}
186
187
	/**
188 3
	 * Get issuers
189 3
	 *
190
	 * @return array<string>
191
	 */
192
	public function get_issuers() {
193
		$response = $this->send_request_to_endpoint( 'methods/ideal?include=issuers', 'GET' );
194
195
		$issuers = array();
196
197
		if ( isset( $response->issuers ) ) {
198
			foreach ( $response->issuers as $issuer ) {
199
				$id   = Security::filter( $issuer->id );
200
				$name = Security::filter( $issuer->name );
201
202
				if ( null === $id || null === $name ) {
203
					continue;
204
				}
205
206
				$issuers[ $id ] = $name;
207
			}
208
		}
209
210
		return $issuers;
211
	}
212
213
	/**
214
	 * Get payment methods
215
	 *
216
	 * @param string $sequence_type Sequence type.
217 2
	 *
218
	 * @return array<string>
219 2
	 * @throws \Exception Throws exception for methods on failed request or invalid response.
220
	 */
221
	public function get_payment_methods( $sequence_type = '' ) {
222 2
		$data = array(
223 2
			'includeWallets' => Methods::APPLE_PAY,
224
		);
225
226 2
		if ( '' !== $sequence_type ) {
227
			$data['sequenceType'] = $sequence_type;
228 2
		}
229
230 2
		$response = $this->send_request_to_endpoint( 'methods', 'GET', $data );
231
232
		$payment_methods = array();
233
234 2
		if ( ! isset( $response->_embedded ) ) {
235 2
			throw new \Exception( 'No embedded data in Mollie response.' );
236 2
		}
237 2
238
		if ( isset( $response->_embedded->methods ) ) {
239 2
			foreach ( $response->_embedded->methods as $payment_method ) {
240
				$id   = Security::filter( $payment_method->id );
241
				$name = Security::filter( $payment_method->description );
242
243 2
				if ( null === $id || null === $name ) {
244
					continue;
245
				}
246
247 2
				$payment_methods[ $id ] = $name;
248
			}
249
		}
250
251
		return $payment_methods;
252
	}
253
254
	/**
255
	 * Create customer.
256
	 *
257
	 * @param Customer $customer Customer.
258
	 * @return Customer
259
	 * @throws Error Throws Error when Mollie error occurs.
260
	 * @since 1.1.6
261
	 */
262
	public function create_customer( Customer $customer ) {
263
		$response = $this->send_request_to_endpoint(
264
			'customers',
265
			'POST',
266
			$customer->get_array()
267
		);
268
269
		if ( \property_exists( $response, 'id' ) ) {
270
			$customer->set_id( $response->id );
271
		}
272
273
		return $customer;
274
	}
275
276
	/**
277
	 * Get customer.
278
	 *
279
	 * @param string $customer_id Mollie customer ID.
280
	 *
281 4
	 * @return null|object
282 4
	 * @throws \InvalidArgumentException Throws exception on empty customer ID argument.
283
	 * @throws Error Throws Error when Mollie error occurs.
284
	 */
285
	public function get_customer( $customer_id ) {
286
		if ( empty( $customer_id ) ) {
287 4
			throw new \InvalidArgumentException( 'Mollie customer ID can not be empty string.' );
288
		}
289
290
		try {
291
			return $this->send_request_to_endpoint( 'customers/' . $customer_id, 'GET' );
292
		} catch ( Error $error ) {
293
			if ( 404 === $error->get_status() ) {
294
				return null;
295
			}
296
297
			throw $error;
298
		}
299
	}
300
301
	/**
302
	 * Create mandate.
303
	 *
304
	 * @param string             $customer_id           Customer ID.
305
	 * @param BankAccountDetails $consumer_bank_details Consumer bank details.
306
	 * @return object
307
	 * @throws Error Throws Error when Mollie error occurs.
308
	 * @since unreleased
309
	 */
310
	public function create_mandate( $customer_id, BankAccountDetails $consumer_bank_details ) {
311
		$response = $this->send_request_to_endpoint(
312
			'customers/' . $customer_id . '/mandates',
313
			'POST',
314
			array(
315
				'method'          => Methods::DIRECT_DEBIT,
316
				'consumerName'    => $consumer_bank_details->get_name(),
317
				'consumerAccount' => $consumer_bank_details->get_iban(),
318
			)
319
		);
320
321
		return $response;
322
	}
323
324
	/**
325
	 * Get mandate.
326
	 *
327
	 * @param string $mandate_id Mollie mandate ID.
328
	 * @param string $customer_id Mollie customer ID.
329
	 * @return object
330
	 * @throws \InvalidArgumentException Throws exception on empty mandate ID argument.
331
	 */
332
	public function get_mandate( $mandate_id, $customer_id ) {
333
		if ( '' === $mandate_id ) {
334
			throw new \InvalidArgumentException( 'Mollie mandate ID can not be empty string.' );
335
		}
336
337
		if ( '' === $customer_id ) {
338
			throw new \InvalidArgumentException( 'Mollie customer ID can not be empty string.' );
339
		}
340
341
		return $this->send_request_to_endpoint( 'customers/' . $customer_id . '/mandates/' . $mandate_id, 'GET' );
342
	}
343
344
	/**
345
	 * Get mandates for customer.
346
	 *
347
	 * @param string $customer_id Mollie customer ID.
348
	 * @return object
349
	 * @throws \InvalidArgumentException Throws exception on empty customer ID argument.
350
	 */
351
	public function get_mandates( $customer_id ) {
352
		if ( '' === $customer_id ) {
353
			throw new \InvalidArgumentException( 'Mollie customer ID can not be empty string.' );
354
		}
355
356
		return $this->send_request_to_endpoint( 'customers/' . $customer_id . '/mandates?limit=250', 'GET' );
357
	}
358
359
	/**
360
	 * Is there a valid mandate for customer?
361
	 *
362
	 * @param string      $customer_id    Mollie customer ID.
363
	 * @param string|null $payment_method Payment method to find mandates for.
364
	 * @param string|null $search         Search.
365
	 *
366
	 * @return string|bool
367
	 * @throws \Exception Throws exception for mandates on failed request or invalid response.
368
	 */
369
	public function has_valid_mandate( $customer_id, $payment_method = null, $search = null ) {
370
		$mandates = $this->get_mandates( $customer_id );
371
372
		$mollie_method = Methods::transform( $payment_method );
373
374
		if ( ! isset( $mandates->_embedded ) ) {
375
			throw new \Exception( 'No embedded data in Mollie response.' );
376
		}
377
378
		foreach ( $mandates->_embedded->mandates as $mandate ) {
379
			if ( null !== $mollie_method && $mollie_method !== $mandate->method ) {
380
				continue;
381
			}
382
383
			// Search consumer account or card number.
384
			if ( null !== $search ) {
385
				switch ( $mollie_method ) {
386
					case Methods::DIRECT_DEBIT:
387
					case Methods::PAYPAL:
388
						if ( $search !== $mandate->details->consumerAccount ) {
389
							continue 2;
390
						}
391
392
						break;
393
					case Methods::CREDITCARD:
394
						if ( $search !== $mandate->details->cardNumber ) {
395
							continue 2;
396
						}
397
398
						break;
399
				}
400
			}
401
402
			if ( 'valid' === $mandate->status ) {
403
				return $mandate->id;
404
			}
405
		}
406
407
		return false;
408
	}
409
410
	/**
411
	 * Get formatted date and time of first valid mandate.
412
	 *
413
	 * @param string $customer_id    Mollie customer ID.
414
	 * @param string $payment_method Payment method.
415
	 *
416
	 * @return null|DateTime
417
	 * @throws \Exception Throws exception for mandates on failed request or invalid response.
418
	 */
419
	public function get_first_valid_mandate_datetime( $customer_id, $payment_method = null ) {
420
		$mandates = $this->get_mandates( $customer_id );
421
422
		$mollie_method = Methods::transform( $payment_method );
423
424
		if ( ! isset( $mandates->_embedded ) ) {
425
			throw new \Exception( 'No embedded data in Mollie response.' );
426
		}
427
428
		foreach ( $mandates->_embedded->mandates as $mandate ) {
429
			if ( $mollie_method !== $mandate->method ) {
430
				continue;
431
			}
432
433
			if ( 'valid' !== $mandate->status ) {
434
				continue;
435
			}
436
437
			if ( ! isset( $valid_mandates ) ) {
438
				$valid_mandates = array();
439
			}
440
441
			// @codingStandardsIgnoreStart
442
			$valid_mandates[ $mandate->createdAt ] = $mandate;
443
			// @codingStandardsIgnoreEnd
444
		}
445
446
		if ( isset( $valid_mandates ) ) {
447
			ksort( $valid_mandates );
448
449
			$mandate = array_shift( $valid_mandates );
450
451
			// @codingStandardsIgnoreStart
452
			$create_date = new DateTime( $mandate->createdAt );
453
			// @codingStandardsIgnoreEnd
454
455
			return $create_date;
456
		}
457
458
		return null;
459
	}
460
461
	/**
462
	 * Create refund.
463
	 *
464
	 * @param string        $payment_id     Mollie payment ID.
465
	 * @param RefundRequest $refund_request Refund request.
466
	 * @return Refund
467
	 */
468
	public function create_refund( $payment_id, RefundRequest $refund_request ) {
469
		$response = $this->send_request_to_endpoint( 'payments/' . $payment_id . '/refunds', 'POST', $refund_request->get_array() );
470
471
		return Refund::from_json( $response );
472
	}
473
474
	/**
475
	 * Get payment chargebacks.
476
	 *
477
	 * @param string               $payment_id Mollie payment ID.
478
	 * @param array<string, mixed> $parameters Parameters.
479
	 * @return array<Chargeback>
480
	 */
481
	public function get_payment_chargebacks( $payment_id, $parameters ) {
482
		$object = $this->send_request_to_endpoint( 'payments/' . $payment_id . '/chargebacks', 'GET', $parameters );
483
484
		$chargebacks = array();
485
486
		if ( \property_exists( $object, '_embedded' ) && \property_exists( $object->_embedded, 'chargebacks' ) ) {
487
			foreach ( $object->_embedded->chargebacks as $chargeback_object ) {
488
				$chargebacks[] = Chargeback::from_json( $chargeback_object );
489
			}
490
		}
491
492
		return $chargebacks;
493
	}
494
}
495