Test Failed
Push — master ( 417a33...505c40 )
by Reüel
16:46 queued 03:16
created

Client::get_mandate()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 10
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 12

Importance

Changes 0
Metric Value
cc 3
eloc 5
nc 3
nop 2
dl 0
loc 10
ccs 0
cts 4
cp 0
crap 12
rs 10
c 0
b 0
f 0
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 ) {
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
				$issuers[ $id ] = $name;
211
			}
212
		}
213
214
		return $issuers;
215
	}
216
217
	/**
218
	 * Get payment methods
219
	 *
220
	 * @param string $sequence_type Sequence type.
221
	 *
222
	 * @return array<string>
223
	 * @throws \Exception Throws exception for methods on failed request or invalid response.
224
	 */
225 2
	public function get_payment_methods( $sequence_type = '' ) {
226
		$data = array(
227 2
			'includeWallets' => Methods::APPLE_PAY,
228
		);
229
230 2
		if ( '' !== $sequence_type ) {
231 2
			$data['sequenceType'] = $sequence_type;
232
		}
233
234 2
		$response = $this->send_request_to_endpoint( 'methods', 'GET', $data );
235
236 2
		$payment_methods = array();
237
238 2
		if ( ! isset( $response->_embedded ) ) {
239
			throw new \Exception( 'No embedded data in Mollie response.' );
240
		}
241
242 2
		if ( isset( $response->_embedded->methods ) ) {
243 2
			foreach ( $response->_embedded->methods as $payment_method ) {
244 2
				$id   = Security::filter( $payment_method->id );
245 2
				$name = Security::filter( $payment_method->description );
246
247 2
				$payment_methods[ $id ] = $name;
248
			}
249
		}
250
251 2
		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
		$customer->set_id( $response->id );
270
271
		return $customer;
272
	}
273
274
	/**
275
	 * Get customer.
276
	 *
277
	 * @param string $customer_id Mollie customer ID.
278
	 *
279
	 * @return null|object
280
	 * @throws \InvalidArgumentException Throws exception on empty customer ID argument.
281
	 * @throws Error Throws Error when Mollie error occurs.
282
	 */
283 4
	public function get_customer( $customer_id ) {
284 4
		if ( empty( $customer_id ) ) {
285
			throw new \InvalidArgumentException( 'Mollie customer ID can not be empty string.' );
286
		}
287
288
		try {
289 4
			return $this->send_request_to_endpoint( 'customers/' . $customer_id, 'GET' );
290
		} catch ( Error $error ) {
291
			if ( 404 === $error->get_status() ) {
292
				return null;
293
			}
294
295
			throw $error;
296
		}
297
	}
298
299
	/**
300
	 * Create mandate.
301
	 *
302
	 * @param string             $customer_id           Customer ID.
303
	 * @param BankAccountDetails $consumer_bank_details Consumer bank details.
304
	 * @return object
305
	 * @throws Error Throws Error when Mollie error occurs.
306
	 * @since unreleased
307
	 */
308
	public function create_mandate( $customer_id, BankAccountDetails $consumer_bank_details ) {
309
		$response = $this->send_request_to_endpoint(
310
			'customers/' . $customer_id . '/mandates',
311
			'POST',
312
			array(
313
				'method'          => Methods::DIRECT_DEBIT,
314
				'consumerName'    => $consumer_bank_details->get_name(),
315
				'consumerAccount' => $consumer_bank_details->get_iban(),
316
			)
317
		);
318
319
		return $response;
320
	}
321
322
	/**
323
	 * Get mandate.
324
	 *
325
	 * @param string $mandate_id Mollie mandate ID.
326
	 * @return object
327
	 * @throws \InvalidArgumentException Throws exception on empty mandate ID argument.
328
	 */
329
	public function get_mandate( $mandate_id, $customer_id ) {
330
		if ( '' === $mandate_id ) {
331
			throw new \InvalidArgumentException( 'Mollie mandate ID can not be empty string.' );
332
		}
333
334
		if ( '' === $customer_id ) {
335
			throw new \InvalidArgumentException( 'Mollie customer ID can not be empty string.' );
336
		}
337
338
		return $this->send_request_to_endpoint( 'customers/' . $customer_id . '/mandates/' . $mandate_id, 'GET' );
339
	}
340
341
	/**
342
	 * Get mandates for customer.
343
	 *
344
	 * @param string $customer_id Mollie customer ID.
345
	 * @return object
346
	 * @throws \InvalidArgumentException Throws exception on empty customer ID argument.
347
	 */
348
	public function get_mandates( $customer_id ) {
349
		if ( '' === $customer_id ) {
350
			throw new \InvalidArgumentException( 'Mollie customer ID can not be empty string.' );
351
		}
352
353
		return $this->send_request_to_endpoint( 'customers/' . $customer_id . '/mandates?limit=250', 'GET' );
354
	}
355
356
	/**
357
	 * Is there a valid mandate for customer?
358
	 *
359
	 * @param string      $customer_id    Mollie customer ID.
360
	 * @param string|null $payment_method Payment method to find mandates for.
361
	 * @param string|null $search         Search.
362
	 *
363
	 * @return boolean
364
	 * @throws \Exception Throws exception for mandates on failed request or invalid response.
365
	 */
366
	public function has_valid_mandate( $customer_id, $payment_method = null, $search = null ) {
367
		$mandates = $this->get_mandates( $customer_id );
368
369
		$mollie_method = Methods::transform( $payment_method );
370
371
		if ( ! isset( $mandates->_embedded ) ) {
372
			throw new \Exception( 'No embedded data in Mollie response.' );
373
		}
374
375
		foreach ( $mandates->_embedded->mandates as $mandate ) {
376
			if ( null !== $mollie_method && $mollie_method !== $mandate->method ) {
377
				continue;
378
			}
379
380
			// Search consumer account or card number.
381
			if ( null !== $search ) {
382
				switch ( $mollie_method ) {
383
					case Methods::DIRECT_DEBIT:
384
					case Methods::PAYPAL:
385
						if ( $search !== $mandate->details->consumerAccount ) {
386
							continue 2;
387
						}
388
389
						break;
390
					case Methods::CREDITCARD:
391
						if ( $search !== $mandate->details->cardNumber ) {
392
							continue 2;
393
						}
394
395
						break;
396
				}
397
			}
398
399
			if ( 'valid' === $mandate->status ) {
400
				return $mandate->id;
401
			}
402
		}
403
404
		return false;
405
	}
406
407
	/**
408
	 * Get formatted date and time of first valid mandate.
409
	 *
410
	 * @param string $customer_id    Mollie customer ID.
411
	 * @param string $payment_method Payment method.
412
	 *
413
	 * @return null|DateTime
414
	 * @throws \Exception Throws exception for mandates on failed request or invalid response.
415
	 */
416
	public function get_first_valid_mandate_datetime( $customer_id, $payment_method = null ) {
417
		$mandates = $this->get_mandates( $customer_id );
418
419
		$mollie_method = Methods::transform( $payment_method );
420
421
		if ( ! isset( $mandates->_embedded ) ) {
422
			throw new \Exception( 'No embedded data in Mollie response.' );
423
		}
424
425
		foreach ( $mandates->_embedded->mandates as $mandate ) {
426
			if ( $mollie_method !== $mandate->method ) {
427
				continue;
428
			}
429
430
			if ( 'valid' !== $mandate->status ) {
431
				continue;
432
			}
433
434
			if ( ! isset( $valid_mandates ) ) {
435
				$valid_mandates = array();
436
			}
437
438
			// @codingStandardsIgnoreStart
439
			$valid_mandates[ $mandate->createdAt ] = $mandate;
440
			// @codingStandardsIgnoreEnd
441
		}
442
443
		if ( isset( $valid_mandates ) ) {
444
			ksort( $valid_mandates );
445
446
			$mandate = array_shift( $valid_mandates );
447
448
			// @codingStandardsIgnoreStart
449
			$create_date = new DateTime( $mandate->createdAt );
450
			// @codingStandardsIgnoreEnd
451
452
			return $create_date;
453
		}
454
455
		return null;
456
	}
457
}
458