Test Failed
Push — master ( 505c40...c5b7ba )
by Reüel
10:20 queued 03:32
created

Client::get_payment_methods()   B

Complexity

Conditions 7
Paths 6

Size

Total Lines 31
Code Lines 16

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 11
CRAP Score 7.0283

Importance

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