Test Failed
Push — develop ( 0e2663...d2614b )
by Remco
07:47
created

src/Gateway.php (4 issues)

Labels
Severity
1
<?php
2
/**
3
 * Gateway
4
 *
5
 * @author    Pronamic <[email protected]>
6
 * @copyright 2005-2021 Pronamic
7
 * @license   GPL-3.0-or-later
8
 * @package   Pronamic\WordPress\Pay\Gateways\OmniKassa2
9
 */
10
11
namespace Pronamic\WordPress\Pay\Gateways\OmniKassa2;
12
13
use Pronamic\WordPress\Money\Money;
0 ignored issues
show
This use statement conflicts with another class in this namespace, Pronamic\WordPress\Pay\Gateways\OmniKassa2\Money. 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...
14
use Pronamic\WordPress\Money\TaxedMoney;
15
use Pronamic\WordPress\Pay\Core\Gateway as Core_Gateway;
16
use Pronamic\WordPress\Pay\Core\PaymentMethods;
17
use Pronamic\WordPress\Pay\Payments\Payment;
18
19
/**
20
 * Gateway
21
 *
22
 * @author  Remco Tolsma
23
 * @version 2.3.4
24
 * @since   1.0.0
25
 */
26
class Gateway extends Core_Gateway {
27
	/**
28
	 * Client.
29
	 *
30
	 * @var Client
31
	 */
32
	private $client;
33
34
	/**
35
	 * Constructs and initializes an OmniKassa 2.0 gateway.
36
	 *
37
	 * @param Config $config Config.
38
	 */
39
	public function __construct( Config $config ) {
40
		parent::__construct( $config );
41
42
		$this->set_method( self::METHOD_HTTP_REDIRECT );
43
44
		// Supported features.
45
		$this->supports = array(
46
			'webhook_log',
47
		);
48
49
		// Client.
50
		$this->client = new Client();
51
52
		$url = Client::URL_PRODUCTION;
53
54
		if ( self::MODE_TEST === $config->mode ) {
55
			$url = Client::URL_SANDBOX;
56
		}
57
58
		$this->client->set_url( $url );
59
		$this->client->set_refresh_token( $config->refresh_token );
60
		$this->client->set_signing_key( $config->signing_key );
61
	}
62
63
	/**
64
	 * Get supported payment methods.
65
	 *
66
	 * @see \Pronamic_WP_Pay_Gateway::get_supported_payment_methods()
67
	 * @return array<string>
68
	 */
69
	public function get_supported_payment_methods() {
70
		return array(
71
			PaymentMethods::AFTERPAY,
72
			PaymentMethods::BANCONTACT,
73
			PaymentMethods::CREDIT_CARD,
74
			PaymentMethods::IDEAL,
75
			PaymentMethods::MAESTRO,
76
			PaymentMethods::MASTERCARD,
0 ignored issues
show
The constant Pronamic\WordPress\Pay\C...mentMethods::MASTERCARD was not found. Maybe you did not declare it correctly or list all dependencies?
Loading history...
77
			PaymentMethods::PAYPAL,
78
			PaymentMethods::V_PAY,
0 ignored issues
show
The constant Pronamic\WordPress\Pay\Core\PaymentMethods::V_PAY was not found. Maybe you did not declare it correctly or list all dependencies?
Loading history...
79
			PaymentMethods::VISA,
0 ignored issues
show
The constant Pronamic\WordPress\Pay\Core\PaymentMethods::VISA was not found. Maybe you did not declare it correctly or list all dependencies?
Loading history...
80
		);
81
	}
82
83
	/**
84
	 * Start.
85
	 *
86
	 * @param Payment $payment Payment.
87
	 * @return void
88
	 * @throws \Exception Throws exception when payment could not start at Rabobank OmniKassa 2.0.
89
	 * @see Core_Gateway::start()
90
	 */
91
	public function start( Payment $payment ) {
92
		// Merchant order ID.
93
		$merchant_order_id = $payment->format_string( $this->config->order_id );
94
95
		$payment->set_meta( 'omnikassa_2_merchant_order_id', $merchant_order_id );
96
97
		// New order.
98
		$merchant_return_url = $payment->get_return_url();
99
100
		/**
101
		 * Filters the OmniKassa 2.0 merchant return URL.
102
		 *
103
		 * OmniKassa 2 requires for each order announcement a merchant return URL.
104
		 * OmniKassa 2 does not allow all merchant return URL's. An order
105
		 * announcement with a merchant return URL's with the TLD `.test` will
106
		 * for example result in the following error:
107
		 *
108
		 * > merchantReturnURL is not a valid web address
109
		 *
110
		 * This can be very inconvenient for testing OmniKassa 2, therefor we
111
		 * introduced this filter.
112
		 *
113
		 * @link https://github.com/wp-pay-gateways/omnikassa-2#pronamic_pay_omnikassa_2_merchant_return_url
114
		 * @link https://github.com/wp-pay-gateways/omnikassa-2/tree/master/documentation#merchantreturnurl-is-not-a-valid-web-address
115
		 * @param string $merchant_return_url Merchant return URL.
116
		 */
117
		$merchant_return_url = \apply_filters( 'pronamic_pay_omnikassa_2_merchant_return_url', $merchant_return_url );
118
119
		$order = new Order(
120
			$merchant_order_id,
121
			MoneyTransformer::transform( $payment->get_total_amount() ),
122
			$merchant_return_url
123
		);
124
125
		// Shipping address.
126
		$order->set_shipping_detail( AddressTransformer::transform( $payment->get_shipping_address() ) );
127
128
		// Billing address.
129
		$order->set_billing_detail( AddressTransformer::transform( $payment->get_billing_address() ) );
130
131
		// Customer information.
132
		$customer = $payment->get_customer();
133
134
		if ( null !== $customer ) {
135
			// Language.
136
			$language = $customer->get_language();
137
138
			if ( null !== $language ) {
139
				$order->set_language( \strtoupper( $language ) );
140
			}
141
142
			// Customer information.
143
			$customer_information = new CustomerInformation();
144
145
			$customer_information->set_email_address( $customer->get_email() );
146
			$customer_information->set_date_of_birth( $customer->get_birth_date() );
147
			$customer_information->set_gender( Gender::transform( $customer->get_gender() ) );
148
			$customer_information->set_telephone_number( $customer->get_phone() );
149
150
			$name = $customer->get_name();
151
152
			if ( null !== $name ) {
153
				$customer_information->set_initials( $name->get_initials() );
154
			}
155
156
			$order->set_customer_information( $customer_information );
157
		}
158
159
		// Payment brand.
160
		$payment_brand = PaymentBrands::transform( $payment->get_method() );
161
162
		$order->set_payment_brand( $payment_brand );
163
164
		if ( null !== $payment_brand ) {
165
			// Payment brand force should only be set if payment brand is not empty.
166
			$order->set_payment_brand_force( PaymentBrandForce::FORCE_ONCE );
167
		}
168
169
		// Description.
170
		$description = $payment->get_description();
171
172
		if ( null !== $description ) {
173
			$order->set_description( DataHelper::sanitize_an( $description, 35 ) );
174
		}
175
176
		// Lines.
177
		$lines = $payment->get_lines();
178
179
		if ( null !== $lines ) {
180
			$order_items = $order->new_items();
181
182
			$i = 1;
183
184
			foreach ( $lines as $line ) {
185
				$name = \sprintf(
186
					/* translators: %s: item index */
187
					\__( 'Item %s', 'pronamic_ideal' ),
188
					$i++
189
				);
190
191
				$line_name = $line->get_name();
192
193
				if ( null !== $line_name && '' !== $line_name ) {
194
					$name = $line_name;
195
				}
196
197
				$unit_price = $line->get_unit_price();
198
199
				if ( null === $unit_price ) {
200
					$unit_price = new Money();
201
				}
202
203
				$item = $order_items->new_item(
204
					DataHelper::sanitize_an( $name, 50 ),
205
					(int) $line->get_quantity(),
206
					// The amount in cents, including VAT, of the item each, see below for more details.
207
					MoneyTransformer::transform( $unit_price ),
208
					ProductCategories::transform( $line->get_type() )
209
				);
210
211
				$item->set_id( $line->get_id() );
212
213
				// Description.
214
				$description = $line->get_description();
215
216
				if ( empty( $description ) && PaymentBrands::AFTERPAY === $payment_brand ) {
217
					/*
218
					 * The `OrderItem.description` field is documented as `0..1` (optional),
219
					 * but for AfterPay payments it is required.
220
					 *
221
					 * @link https://github.com/wp-pay-gateways/omnikassa-2/tree/feature/post-pay/documentation#error-5024
222
					 */
223
					$description = $name;
224
				}
225
226
				if ( null !== $description ) {
227
					$description = DataHelper::sanitize_an( $description, 100 );
228
				}
229
230
				$item->set_description( $description );
231
232
				if ( $unit_price instanceof TaxedMoney ) {
233
					$tax_amount = $unit_price->get_tax_amount();
234
235
					if ( null !== $tax_amount ) {
236
						// The VAT of the item each, see below for more details.
237
						$item->set_tax( MoneyTransformer::transform( $tax_amount ) );
238
					}
239
				}
240
			}
241
		}
242
243
		// Maybe update access token.
244
		$this->maybe_update_access_token();
245
246
		// Announce order.
247
		$response = $this->client->order_announce( $this->config, $order );
248
249
		$payment->set_transaction_id( $response->get_omnikassa_order_id() );
250
		$payment->set_action_url( $response->get_redirect_url() );
251
	}
252
253
	/**
254
	 * Update status of the specified payment.
255
	 *
256
	 * @param Payment $payment Payment.
257
	 * @return void
258
	 */
259
	public function update_status( Payment $payment ) {
260
		// phpcs:ignore WordPress.Security.NonceVerification.Recommended, SlevomatCodingStandard.Variables.DisallowSuperGlobalVariable.DisallowedSuperGlobalVariable
261
		if ( ! ReturnParameters::contains( $_GET ) ) {
262
			return;
263
		}
264
265
		// phpcs:ignore WordPress.Security.NonceVerification.Recommended, SlevomatCodingStandard.Variables.DisallowSuperGlobalVariable.DisallowedSuperGlobalVariable
266
		$parameters = ReturnParameters::from_array( $_GET );
267
268
		// Note.
269
		$note_values = array(
270
			'order_id'  => $parameters->get_order_id(),
271
			'status'    => $parameters->get_status(),
272
			'signature' => (string) $parameters->get_signature(),
273
			'valid'     => $parameters->is_valid( $this->config->signing_key ) ? 'true' : 'false',
274
		);
275
276
		$note = '';
277
278
		$note .= '<p>';
279
		$note .= \__( 'OmniKassa 2.0 return URL requested:', 'pronamic_ideal' );
280
		$note .= '</p>';
281
282
		$note .= '<dl>';
283
284
		foreach ( $note_values as $key => $value ) {
285
			$note .= \sprintf( '<dt>%s</dt>', \esc_html( $key ) );
286
			$note .= \sprintf( '<dd>%s</dd>', \esc_html( $value ) );
287
		}
288
289
		$note .= '</dl>';
290
291
		$payment->add_note( $note );
292
293
		// Validate.
294
		if ( ! $parameters->is_valid( $this->config->signing_key ) ) {
295
			return;
296
		}
297
298
		// Status.
299
		$pronamic_status = Statuses::transform( $parameters->get_status() );
300
301
		if ( null !== $pronamic_status ) {
302
			$payment->set_status( $pronamic_status );
303
		}
304
	}
305
306
	/**
307
	 * Handle notification.
308
	 *
309
	 * @param Notification $notification Notification.
310
	 * @return void
311
	 * @throws \Pronamic\WordPress\Pay\Gateways\OmniKassa2\InvalidSignatureException Throws invalid signature exception when notification message does not match gateway configuration signature.
312
	 */
313
	public function handle_notification( Notification $notification ) {
314
		if ( ! $notification->is_valid( $this->config->signing_key ) ) {
315
			throw new \Pronamic\WordPress\Pay\Gateways\OmniKassa2\InvalidSignatureException(
316
				\sprintf(
317
					'Signature on notification message does not match gateway configuration signature (%s).',
318
					\substr( $this->config->signing_key, 0, 7 )
319
				)
320
			);
321
		}
322
323
		switch ( $notification->get_event_name() ) {
324
			case 'merchant.order.status.changed':
325
				$this->handle_merchant_order_status_changed( $notification );
326
		}
327
	}
328
329
	/**
330
	 * Handle `merchant.order.status.changed` event.
331
	 *
332
	 * @param Notification $notification Notification.
333
	 * @return void
334
	 * @throws \Pronamic\WordPress\Pay\Gateways\OmniKassa2\InvalidSignatureException Throws invalid signature exception when order results message does not match gateway configuration signature.
335
	 * @throws \Pronamic\WordPress\Pay\Gateways\OmniKassa2\UnknownOrderIdsException Throws unknow order IDs exception when no payment could be find for on ore more OmniKassa order IDs.
336
	 */
337
	private function handle_merchant_order_status_changed( Notification $notification ) {
338
		$unknown_order_ids = array();
339
340
		do {
341
			$order_results = $this->client->get_order_results( $notification->get_authentication() );
342
343
			if ( ! $order_results->is_valid( $this->config->signing_key ) ) {
344
				throw new \Pronamic\WordPress\Pay\Gateways\OmniKassa2\InvalidSignatureException(
345
					\sprintf(
346
						'Signature on order results message does not match gateway configuration signature (%s).',
347
						\substr( $this->config->signing_key, 0, 7 )
348
					)
349
				);
350
			}
351
352
			foreach ( $order_results as $order_result ) {
353
				$omnikassa_order_id = $order_result->get_omnikassa_order_id();
354
355
				$payment = \get_pronamic_payment_by_transaction_id( $omnikassa_order_id );
356
357
				if ( empty( $payment ) ) {
358
					/**
359
					 * If there is no payment found with the OmniKassa order ID
360
					 * we will continue to check the other order results. It is
361
					 * possible that the payment has been deleted and can
362
					 * therefore no longer be updated. We keep track of this
363
					 * exception and throw it at the end of this function.
364
					 */
365
					$unknown_order_ids[] = $omnikassa_order_id;
366
367
					continue;
368
				}
369
370
				/**
371
				 * Webhook log payment.
372
				 *
373
				 * The `pronamic_pay_webhook_log_payment` action is triggered so the
374
				 * `wp-pay/core` library can hook into this and register the webhook
375
				 * call.
376
				 *
377
				 * @param Payment $payment Payment to log.
378
				 */
379
				\do_action( 'pronamic_pay_webhook_log_payment', $payment );
380
381
				$pronamic_status = Statuses::transform( $order_result->get_order_status() );
382
383
				if ( null !== $pronamic_status ) {
384
					$payment->set_status( $pronamic_status );
385
				}
386
387
				// Note.
388
				$note = \sprintf(
389
					'<p>%s</p><pre>%s</pre>',
390
					\__( 'OmniKassa 2.0 webhook URL requested:', 'pronamic_ideal' ),
391
					(string) \wp_json_encode( $order_result, \JSON_PRETTY_PRINT )
392
				);
393
394
				$payment->add_note( $note );
395
396
				$payment->save();
397
			}
398
		} while ( $order_results->more_available() );
399
400
		if ( \count( $unknown_order_ids ) > 0 ) {
401
			throw new \Pronamic\WordPress\Pay\Gateways\OmniKassa2\UnknownOrderIdsException(
402
				\sprintf(
403
					'Could not find payments for the following OmniKassa order IDs: %s.',
404
					\implode( ', ', $unknown_order_ids )
405
				)
406
			);
407
		}
408
	}
409
410
	/**
411
	 * Maybe update access token.
412
	 *
413
	 * @return void
414
	 */
415
	private function maybe_update_access_token() {
416
		if ( $this->config->is_access_token_valid() ) {
417
			return;
418
		}
419
420
		$data = $this->client->get_access_token_data();
421
422
		if ( isset( $data->token ) ) {
423
			$this->config->access_token = $data->token;
424
425
			\update_post_meta( $this->config->post_id, '_pronamic_gateway_omnikassa_2_access_token', $data->token );
426
		}
427
428
		/*
429
		 * @codingStandardsIgnoreStart
430
		 *
431
		 * Ignore coding standards because of sniff WordPress.NamingConventions.ValidVariableName.NotSnakeCaseMemberVar
432
		 */
433
		if ( isset( $data->validUntil ) ) {
434
			$this->config->access_token_valid_until = $data->validUntil;
435
436
			\update_post_meta(
437
				$this->config->post_id,
438
				'_pronamic_gateway_omnikassa_2_access_token_valid_until',
439
				$data->validUntil
440
			);
441
		}
442
		// @codingStandardsIgnoreEnd
443
	}
444
}
445