PluginProbe
HyperPay Payments / trunk
HyperPay Payments vtrunk
6.6.0 trunk 1.7 1.8 1.8.1 1.8.2 1.8.3 1.8.4 1.8.5 1.9.5 1.9.6 1.9.7 1.9.8 1.9.9 2.0.0 2.1.0 2.2.0 2.3.0 2.3.1 2.3.2 2.3.3 3.0.0 3.0.5 4.0.0 4.0.2 All 34 releases
hyperpay-gateways / src / App / DefaultGateway.php

DefaultGateway.php in HyperPay Payments trunk, at src/App/DefaultGateway.php

1,195 lines 39.9 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 namespace Hyperpay\Gateways\App;
4
5 if (!defined('ABSPATH')) exit;
6
7
8 use Exception;
9 use Hyperpay\Gateways\Helpers\Http;
10 use Hyperpay\Gateways\Helpers\Log;
11 use Hyperpay\Gateways\Helpers\SubscriptionsManager;
12 use Hyperpay\Gateways\Helpers\TokenManager;
13 use Hyperpay\Gateways\Helpers\View;
14 use Hyperpay\Gateways\Main;
15 use Hyperpay\Gateways\Traits\HyperpayBlocks;
16 use WC_Order;
17 use WC_Payment_Gateway;
18 use Hyperpay\Gateways\Traits\HasTokenization;
19
20
21 /**
22 * Hyperpay main class created to extends from it
23 * when create a new payments Gateways
24 *
25 */
26 class DefaultGateway extends WC_Payment_Gateway
27 {
28 use HyperpayBlocks;
29
30 /**
31 * Gateway admin options
32 */
33 public $testMode, $title, $trans_type;
34 public $accessToken, $entityId, $brands, $order_status;
35 public $custom_style, $latin_validation;
36 public $currency, $is_arabic, $supported_network;
37 public $server_to_server = false;
38
39
40 /**
41 * connecter type on test mode
42 *
43 * @var string INTERNAL|EXTERNAL
44 */
45
46 public $trans_mode = 'EXTERNAL';
47 public $description;
48 public $instructions;
49 public $form_fields = [];
50
51 /**
52 * if payments have direct fields on checkout page
53 *
54 * @var boolean
55 */
56 public $has_fields = false;
57
58 /**
59 * used to display the invoice id
60 * at success order page
61 * @var string|null
62 */
63 public $invoice_id;
64
65
66 public $id;
67
68 /**
69 * Mada BlackBins
70 *
71 * @var array
72 */
73 protected $blackBins = [];
74
75 /**
76 * supported brands thats will showing on settings and checkout page
77 *
78 * @var array
79 */
80 protected $supported_brands = [];
81
82 /**
83 * displayed error msg
84 * @var string
85 */
86 protected $failed_message = '';
87
88 /**
89 * displayed success msg
90 * @var string
91 */
92 protected $success_message = '';
93
94 public $NONCE = '';
95
96 /**
97 * regular expressions
98 * @var string
99 */
100
101 public $successCodePattern = '/^(000\.000\.|000\.100\.1|000\.[36])/';
102 public $successManualReviewCodePattern = '/^(000\.400\.0|000\.400\.100)/';
103 public $pendingCodePattern = '/^(800\.400\.5|100\.400\.500)/';
104
105 /**
106 * CopyAndPay script URL
107 *
108 * @var string
109 */
110 public $script_url = "https://eu-prod.oppwa.com/v1/paymentWidgets.js?checkoutId=";
111
112 /**
113 * CopyAndPay prepare checkout link
114 *
115 * @method POST
116 * @var string
117 */
118 protected $token_url = "https://eu-prod.oppwa.com/v1/checkouts";
119
120
121 /**
122 * get transaction status
123 * @method GET
124 * @var string
125 *
126 * ##TOKEN## will replace with transaction id when fire the request
127 */
128 protected $transaction_status_url = "https://eu-prod.oppwa.com/v1/checkouts/##TOKEN##/payment";
129
130 /**
131 * back-office end-point
132 * @method GET
133 * @var string
134 *
135 */
136 protected $server_to_server_url = "https://eu-prod.oppwa.com/v1/payments";
137
138 /**
139 * Query transaction report
140 *
141 * @method GET
142 * @var string
143 */
144 protected $query_url = "https://eu-prod.oppwa.com/v1/query";
145
146
147 protected $ACI_base_url = "https://eu-prod.oppwa.com";
148 /**
149 * payment styles that will show in settings
150 *
151 * @var array
152 *
153 */
154 protected $payment_style = [
155 'card' => 'Card',
156 'plain' => 'Plain'
157 ];
158
159
160 public function boot() {}
161
162 public function __construct()
163 {
164
165 $this->init_settings(); // <== to get saved settings from database
166 $this->init_form_fields(); // <== render form inside admin panel
167 $this->is_arabic = substr(get_locale(), 0, 2) == 'ar'; // <== to get current locale
168
169 $this->testMode = $this->get_option('testmode'); // <== check if payments on test mode
170 $this->title = $this->get_option('title'); // <== get title from setting
171 $this->trans_type = $this->get_option('trans_type'); // <== get transaction type [DB / Pre-Auth] from setting
172 $this->accessToken = $this->get_option('accesstoken'); // <== get access toke from setting
173 $this->entityId = $this->getEntity(); // <== get entityId from setting
174
175 $this->brands = is_array($this->get_option('hyper_pay_brands')) ? $this->get_option('hyper_pay_brands') : [$this->get_option('hyper_pay_brands')]; // <== get brands from setting
176
177 $this->payment_style = $this->get_option('payment_style'); // <== get style from setting
178
179 $this->order_status = $this->get_option('order_status'); // <== get order status after success from setting
180 $this->custom_style = $this->get_option('custom_style'); // <== get custom style from setting
181
182 $this->description = __('All transactions are processed in a secure environment.', 'hyperpay-gateways');
183
184
185 $this->latin_validation = $this->get_option('latin_validation'); // <== get custom style from setting
186 $this->currency = get_woocommerce_currency();
187
188 $this->NONCE = \md5(wp_rand(1111111111, 9999999999));
189
190
191 /**
192 * if test mode is one
193 * overwrite currents URLs ti test URLs
194 */
195 if ($this->testMode) {
196 $this->query_url = "https://eu-test.oppwa.com/v1/query";
197 $this->token_url = "https://eu-test.oppwa.com/v1/checkouts";
198 $this->script_url = "https://eu-test.oppwa.com/v1/paymentWidgets.js?checkoutId=";
199 $this->transaction_status_url = "https://eu-test.oppwa.com/v1/checkouts/##TOKEN##/payment";
200 $this->server_to_server_url = "https://eu-test.oppwa.com/v1/payments";
201 $this->ACI_base_url = "https://eu-test.oppwa.com";
202 }
203
204 $this->query_url .= "?entityId=" . $this->entityId;
205 $this->transaction_status_url .= "?entityId=" . $this->entityId;
206
207 /**
208 * default failed message
209 * @var string
210 */
211 $this->failed_message = __('Your transaction not completed .', 'hyperpay-gateways');
212 $this->success_message = __('Your payment has been processed successfully.', 'hyperpay-gateways');
213
214 /**
215 * overwrite default update function
216 *
217 * @param woocommerce_update_options_payment_gateways_<payment_id>
218 * @param array[class,function_name]
219 */
220
221 if (!has_action("woocommerce_update_options_payment_gateways_{$this->id}")) {
222 add_action('woocommerce_update_options_payment_gateways_' . $this->id, array($this, 'process_admin_options'));
223 }
224
225
226 /**
227 * prepare checkout form
228 *
229 * @param string woocommerce_receipt_<payments_id>
230 * @param array[class,function_name]
231 */
232
233 if (!has_action("woocommerce_receipt_{$this->id}")) {
234 add_action("woocommerce_receipt_{$this->id}", [$this, 'receipt_page']);
235 }
236
237
238 /**
239 * set payments icon from src/assets/images/BRAND-log.png
240 *
241 * make sure when add new image to rename image according this format BRAND_NAME-logo.svg
242 *
243 * @param string woocommerce_gateway_icon
244 * @param array[class,function_name]
245 *
246 */
247 add_filter('woocommerce_gateway_icon', [$this, 'set_icons'], 10, 2);
248
249 /**
250 * to include src/assets/js/admin.js <JavaScript>
251 *
252 * @param string admin_enqueue_scripts
253 * @param array[class,function_name]
254 *
255 */
256 add_action('admin_enqueue_scripts', [$this, 'admin_script']);
257
258 add_action("woocommerce_thankyou_order_received_text", [$this, "order_received_text"], 10, 2);
259
260 add_action('before_woocommerce_pay', [$this, 'action_before_woocommerce_pay'], 10, 0);
261
262 if (!has_action("woocommerce_order_action_capture_payment")) {
263 add_action('woocommerce_order_action_capture_payment', [$this, 'capture_payment']);
264 }
265
266 $this->boot();
267 $this->bootTraits();
268 }
269
270 /**
271 * Automatically calls all protected bootTraitName() methods from used traits.
272 */
273 protected function bootTraits(): void
274 {
275 $traits = class_uses($this, false); // false = don't autoload, if not needed
276
277 foreach ($traits as $fullyQualifiedTraitName) {
278
279 // Extract the short name of the trait
280 $parts = explode('\\', $fullyQualifiedTraitName);
281 $traitName = end($parts);
282
283 // Construct the expected method name
284 $bootMethod = 'boot' . $traitName;
285 if (method_exists($this, $bootMethod)) {
286 $this->$bootMethod();
287 }
288 }
289 }
290
291 public function getEntity()
292 {
293 $available_currencies = $this->get_option('currencies_ids');
294 $current_currency = get_woocommerce_currency();
295
296 if (isset($available_currencies[$current_currency])) {
297 return $available_currencies[$current_currency];
298 }
299
300 return $this->get_option('entityId');
301 }
302
303
304 public function process_admin_options()
305 {
306 $this->init_settings();
307
308 $post_data = $this->get_post_data();
309
310 foreach ($this->get_form_fields() as $key => $field) {
311 if ('title' !== $this->get_field_type($field)) {
312 try {
313 $this->settings[$key] = $this->get_field_value($key, $field, $post_data);
314 if ('select' === $field['type'] || 'checkbox' === $field['type']) {
315 /**
316 * Notify that a non-option setting has been updated.
317 *
318 * @since 7.8.0
319 */
320 do_action(
321 'woocommerce_update_non_option_setting',
322 array(
323 'id' => $key,
324 'type' => $field['type'],
325 'value' => $this->settings[$key],
326 )
327 );
328 } elseif ('currencies_ids_field' === $key && isset($post_data['currencies_ids'])) {
329 $this->settings["currencies_ids"] = array_reduce($post_data['currencies_ids'], function ($result, $item) {
330 if ($item['value'] && $item['name'])
331 $result[$item['name']] = $item['value'];
332 return $result;
333 }, array());
334 }
335 } catch (Exception $e) {
336 $this->add_error($e->getMessage());
337 }
338 }
339 }
340
341 $option_key = $this->get_option_key();
342 do_action('woocommerce_update_option', array('id' => $option_key)); // phpcs:ignore WooCommerce.Commenting.CommentHooks.MissingHookComment
343 return update_option($option_key, apply_filters('woocommerce_settings_api_sanitized_fields_' . $this->id, $this->settings), 'yes'); // phpcs:ignore WooCommerce.Commenting.CommentHooks.MissingHookComment
344 }
345
346 public function capture_payment($order)
347 {
348
349
350 $orderAmount = number_format($order->get_total(), 2, '.', '');
351 $amount = number_format(round($orderAmount, 2), 2, '.', '');
352
353 $gateways = WC()->payment_gateways->payment_gateways();
354
355 $gateway = null;
356
357 foreach ($gateways as $wc_gateway) {
358 if ($wc_gateway->id == $order->get_payment_method()) {
359 $gateway = $wc_gateway;
360 break;
361 }
362 }
363
364 if ($gateway) {
365 $uniqueId = $order->get_meta('transaction_id');
366 $url = $gateway->server_to_server_url . "/$uniqueId";
367
368 $data = $gateway->getBasicData();
369 $data['body'] = \array_merge($data['body'], [
370 "amount" => $amount,
371 "currency" => $gateway->currency,
372 "paymentType" => 'CP'
373 ]);
374
375 $response = Http::post($url, $data);
376
377 $resultCode = $response['result']['code'] ?? '';
378
379 if (preg_match($gateway->successCodePattern, $resultCode) || preg_match($gateway->successManualReviewCodePattern, $resultCode)) {
380 $order->add_order_note("Captured Successfully");
381 $order->update_status($gateway->order_status);
382 } else {
383 $order->add_order_note("Captured Failed " . $response['result']['description'] ?? 'Unknown reason');
384 }
385 } else {
386 $order->add_order_note("Captured Failed No gateway found");
387 }
388
389 // Safely check and unslash HTTP_REFERER
390 $location = isset($_SERVER['HTTP_REFERER']) ? sanitize_url(wp_unslash($_SERVER['HTTP_REFERER'])) : home_url();
391 wp_safe_redirect($location);
392 exit();
393 }
394
395 public function auto_revisal($id)
396 {
397
398 $url = $this->server_to_server_url . "/$id";
399
400 $data = $this->getBasicData();
401 $data['body'] = \array_merge($data['body'], [
402 "amount" => TokenManager::TOKENIZATION_AMOUNT,
403 "currency" => $this->currency,
404 "paymentType" => 'RV'
405 ]);
406
407
408 $response = Http::post($url, $data);
409 $resultCode = $response['result']['code'] ?? '';
410
411 if (preg_match($this->successCodePattern, $resultCode) || preg_match($this->successManualReviewCodePattern, $resultCode)) {
412 return [
413 'status' => 'success',
414 'message' => "Auto-Reversal Successfully"
415 ];
416 }
417
418 return [
419 'status' => 'failed',
420 'message' => "Auto-Reversal Failed . " . ($response['result']['description'] ?? 'Unknown reason')
421 ];
422 }
423
424 public function action_before_woocommerce_pay()
425 {
426 global $wp;
427
428 $order_id = absint($wp->query_vars['order-pay']); // The order ID
429
430 $order = wc_get_order($order_id);
431
432 if ($order->has_status('on-hold')) {
433 $order->update_status('pending');
434 } elseif ($order->has_status($this->order_status)) {
435 wp_safe_redirect($this->get_return_url($order));
436 exit();
437 }
438 }
439
440 public function order_received_text($thanks_text, $order)
441 {
442
443 $msg = $order->get_meta('gateway_note');
444 if ($order->get_payment_method() == $this->id && $order->get_status() == 'on-hold' && !empty($msg)) {
445 wc_add_notice($msg, "notice");
446 wc_print_notices();
447 } else {
448 return $thanks_text;
449 }
450 }
451
452 /**
453 * for validate settings form
454 * @return void
455 */
456
457 public function admin_script(): void
458 {
459 global $current_tab, $current_section;
460
461 /**
462 * to make sure load admin.js just when currents payments opened
463 *
464 */
465 if ($current_tab == 'checkout' && $current_section == $this->id) {
466
467 $data = [
468 'id' => $this->id,
469 'url' => $this->token_url,
470 'code_setting' => wp_enqueue_code_editor(['type' => 'text/css'])
471 ];
472
473 wp_enqueue_script('hyperpay_admin', HYPERPAY_PLUGIN_DIR . '/src/assets/js/admin.js', ['jquery'], '1.0.0', true);
474 wp_localize_script('hyperpay_admin', 'hyperpay_data', $data);
475 }
476 }
477
478
479 public function iconSrc()
480 {
481 $icons = [];
482 foreach ($this->brands as $brand) {
483 $img = HYPERPAY_PLUGIN_DIR . '/src/assets/images/default.png';
484 if (file_exists(Main::ROOT_PATH . '/assets/images/' . esc_attr($brand) . "-logo.svg"))
485 $img = HYPERPAY_PLUGIN_DIR . '/src/assets/images/' . esc_attr($brand) . "-logo.svg";
486
487 $icons[] = $img;
488 }
489 return $icons;
490 }
491
492 /**
493 * to set payment icon based on supported brands
494 *
495 * @param string $icon
496 * @param string $id current payment id
497 *
498 * @return string $icon new icon
499 *
500 */
501
502 public function set_icons($icon, $id)
503 {
504
505 if ($id == $this->id) {
506 $icons = "";
507 foreach ($this->iconSrc() as $src) {
508 $icons .= "<img style='padding:2px ; ' src='$src' >";
509 }
510 return $icons;
511 }
512
513 return $icon;
514 }
515
516 /**
517 * Here you can define all fields thats will showing in setting page
518 * @return void
519 */
520 public function init_form_fields(): void
521 {
522
523 $this->form_fields = [
524 'enabled' => [
525 'title' => __('Enable/Disable', 'hyperpay-gateways'),
526 'type' => 'checkbox',
527 'label' => __('Enable Payment Module.', 'hyperpay-gateways'),
528 'default' => 'no'
529 ],
530 'testmode' => [
531 'title' => __('Test mode', 'hyperpay-gateways'),
532 'type' => 'select',
533 'options' => ['0' => __('Off', 'hyperpay-gateways'), '1' => __('On', 'hyperpay-gateways')]
534 ],
535 'title' => [
536 'title' => __('Title:', 'hyperpay-gateways'),
537 'type' => 'text',
538 'description' => __('This controls the title which the user sees during checkout.', 'hyperpay-gateways'),
539 'default' => $this->method_title ?? __('Credit Card', 'hyperpay-gateways')
540 ],
541 'trans_type' => [
542 'title' => __('Transaction type', 'hyperpay-gateways'),
543 'type' => 'select',
544 'options' => $this->get_hyperpay_trans_type(),
545 ],
546 'accesstoken' => [
547 'title' => __('Access Token', 'hyperpay-gateways'),
548 'type' => 'text',
549 ],
550 'entityId' => [
551 'title' => __('Entity ID', 'hyperpay-gateways'),
552 'type' => 'text',
553 'description' => __('This will used as default if multi-currency not configured', 'hyperpay-gateways'),
554 ],
555 'currencies_ids_field' => [
556 'custom_attributes' => [
557 'data-currencies' => $this->get_option("currencies_ids") ? json_encode($this->get_option("currencies_ids")) : null,
558 'data-currencies_list' => json_encode(array_keys(get_woocommerce_currencies()))
559 ],
560 'title' => __('Multi-currency', 'hyperpay-gateways'),
561 'type' => 'hidden',
562 'description' => __('In case you have a multi-currency store', 'hyperpay-gateways'),
563 ],
564 'secret' => [
565 'title' => __('Webhook Key', 'hyperpay-gateways'),
566 'type' => 'text',
567 ],
568 '_webhock' => [
569 'title' => __('Webhook URL', 'hyperpay-gateways'),
570 'type' => 'text',
571 'class' => 'disabled',
572 'default' => get_site_url() . "/?rest_route=/hyperpay/v1/" . \str_replace("\\", "/", get_class($this))
573 ],
574 'hyper_pay_brands' => [
575 'title' => __('Brands', 'hyperpay-gateways'),
576 'class' => count($this->supported_brands) !== 1 ?: 'disabled',
577 'type' => count($this->supported_brands) > 1 ? 'multiselect' : 'select',
578 'options' => $this->supported_brands,
579 ],
580 'payment_style' => [
581 'title' => __('Payment Style', 'hyperpay-gateways'),
582 'type' => 'select',
583 'class' => count($this->payment_style) !== 1 ?: 'disabled',
584 'options' => $this->payment_style,
585 'default' => 'plain'
586 ],
587 'custom_style' => [
588 'title' => __('Custom Style', 'hyperpay-gateways'),
589 'type' => 'textarea',
590 'description' => 'Input custom css for payment (Optional)',
591 'class' => 'hyperpay_custom_style'
592 ],
593 'latin_validation' => [
594 'title' => __('Enable Input validation (Accept English Characters only)', 'hyperpay-gateways'),
595 'type' => 'checkbox',
596 'label' => __('Yes', 'hyperpay-gateways'),
597 'default' => 'yes',
598 'description' => __('Disable this option may cause transaction declined by bank due to 3DSecure', 'hyperpay-gateways'),
599 ],
600 'order_status' => [
601 'title' => __('Status Of Order', 'hyperpay-gateways'),
602 'type' => 'select',
603 'options' => $this->get_order_status(),
604 'description' => __("select order status after success transaction.", 'hyperpay-gateways')
605 ],
606
607 ];
608 }
609
610
611 /**
612 * to fill order_status select fields
613 *
614 * @return array
615 */
616 function get_order_status(): array
617 {
618 $order_status = [
619
620 'processing' => __('Processing', 'hyperpay-gateways'),
621 'completed' => __('Completed', 'hyperpay-gateways')
622 ];
623
624 return $order_status;
625 }
626
627 /**
628 * to fill trans_type select fields
629 *
630 * @return array
631 */
632 function get_hyperpay_trans_type(): array
633 {
634 $hyperpay_trans_type = [
635 'DB' => 'Debit',
636 'PA' => 'Pre-Authorization'
637 ];
638
639 return $hyperpay_trans_type;
640 }
641
642
643 /**
644 * This function fire when click on Place order at checkout page
645 * @param int $order_id
646 *
647 * @return void
648 */
649 function receipt_page($order_id)
650 {
651
652 $order = new WC_Order($order_id);
653
654 // if we have id param that mean the page result ACI redirection
655 if (isset($_GET['resourcePath'])) {
656 // Verify nonce for security (Recommended)
657 if (!isset($_GET['nonce']) || !wp_verify_nonce(sanitize_text_field(wp_unslash($_GET['nonce'])), 'hyperpay_receipt_nonce')) {
658 //TODO:
659 }
660
661 $resourcePath = sanitize_text_field(wp_unslash($_GET['resourcePath']));
662 $url = $this->ACI_base_url . $resourcePath;
663 // set header request to contain access token
664 $auth = $this->getAuthData();
665
666 $response = Http::get($url, $auth);
667
668
669 //Dynamic fire function based on status
670 $status = $this->check_status($response);
671 return $this->$status($order, $response);
672 } elseif ($result = $this->prepareCheckout($order_id)) { // process a new transaction
673 return $this->renderPaymentForm($order, $result);
674 }
675 }
676
677 private function isJson($string)
678 {
679 json_decode($string);
680 return json_last_error() === JSON_ERROR_NONE;
681 }
682
683 /**
684 *
685 * render CopyAndPay form
686 * @param WC_Order $order
687 * @param string $token
688 * @return void
689 */
690 public function renderPaymentForm(WC_Order $order, $result)
691 {
692
693 if ($this->server_to_server) {
694 $redirect = $result['response']['redirect'];
695 $extra_params = [];
696 $query_url = wp_parse_url($redirect['url'], PHP_URL_QUERY);
697 if ($query_url)
698 parse_str(wp_parse_url($redirect['url'], PHP_URL_QUERY), $extra_params);
699
700
701 return View::render('server-to-server.html', \compact('redirect', 'extra_params'));
702 }
703
704 $token = $result['response']['id'];
705 $postBackURL = $result['postBackURL'];
706
707 $payment_brands = $this->brands;
708
709 if (is_array($this->brands))
710 $payment_brands = implode(' ', $this->brands);
711
712
713 $dataObj = [
714 'is_arabic' => esc_js($this->is_arabic),
715 'style' => esc_html($this->payment_style),
716 'postBackURL' => ($postBackURL),
717 'payment_brands' => esc_html($payment_brands),
718 'custom_style' => esc_html($this->custom_style),
719 'scriptURL' => esc_html($this->script_url),
720 'checkoutId' => $token,
721 'integrity' => esc_html($result['integrity'] ?? ''),
722 'is_subscription' => SubscriptionsManager::orderContainsSubscription($order) ? 'yes' : 'no',
723 'orderId' => $order->id,
724 'hyperpay_update_nonce' => wp_create_nonce('hyperpay_update_checkout'),
725 'hyperpay_process_checkout_nonce' => wp_create_nonce('hyperpay_process_checkout'),
726 'nonce' => $this->NONCE,
727 'id' => $this->id
728 ];
729
730
731 if ($this->supported_network) {
732 $dataObj['supported_network'] = $this->supported_network;
733 }
734
735
736 add_action('wp_head', [$this, 'custom_add_to_head']);
737
738 $scriptSrc = in_array('tokenization', $this->supports, true) ? "script.js" : "script_no_tokenization.js";
739
740 return View::render('copy-and-pay.html', compact('dataObj', 'scriptSrc'));
741 }
742
743 public function custom_add_to_head()
744 {
745
746 $data = [
747 'url' => $this->ACI_base_url,
748 'home' => get_home_url(),
749 'nonce' => $this->NONCE
750 ];
751
752 return View::render('header.html', \compact('data'));
753 }
754
755
756 /**
757 * Process the payment and return the result
758 * @param int $order_id
759 * @return array[redirect,token,result]
760 *
761 */
762 public function process_payment($order_id)
763 {
764
765 $order = new WC_Order($order_id);
766 /**
767 *
768 * validate data to prevent arabic character
769 */
770
771 if ($this->latin_validation == 'yes') {
772 $firstName = $order->get_billing_first_name();
773 $family = $order->get_billing_last_name();
774 $street = $order->get_billing_address_1();
775 $city = $order->get_billing_city();
776 $email = $order->get_billing_email();
777
778
779 $data_to_validate = [
780 'first name' => $firstName,
781 'last name' => $family,
782 'street' => $street,
783 'city' => $city,
784 'email' => $email,
785 ];
786
787 if ($order->get_billing_state()) {
788 $data_to_validate['state'] = $order->get_billing_state();
789 }
790
791
792 // raise a validation error if validation valid
793 $this->validate_form($data_to_validate);
794 }
795
796
797 return [
798 'result' => 'success',
799 'redirect' => $order->get_checkout_payment_url(true)
800 ];
801 }
802
803 protected function getAuthData()
804 {
805 return [
806 'headers' => ["Authorization" => "Bearer {$this->accessToken}"],
807 "body" => ["entityId" => $this->entityId]
808 ];
809 }
810
811 protected function getBasicData()
812 {
813 $data = $this->getAuthData();
814
815 if ($this->testMode) {
816 $data['body']["testMode"] = $this->trans_mode;
817 $data['body']["customParameters[3DS2_enrolled]"] = "true";
818 $data['body']["customParameters[3DS2_flow]"] = 'challenge';
819 }
820
821 return $data;
822 }
823
824 protected function buildCheckoutParams(WC_Order $order)
825 {
826
827 $shipping_cost = number_format($order->get_shipping_total(), 2, '.', '');
828 $amount = number_format($order->get_total(), 2, '.', '');
829 $basicData = $this->getBasicData();
830
831 $data = [
832 "amount" => $amount,
833 "currency" => $this->currency,
834 "paymentType" => $this->trans_type,
835 "customer.email" => $order->get_billing_email(),
836 "notificationUrl" => $order->get_checkout_payment_url(true),
837 "customParameters[bill_number]" => $order->get_id(),
838 "customer.givenName" => $order->get_billing_first_name(),
839 "customer.surname" => $order->get_billing_last_name(),
840 "billing.street1" => $order->get_billing_address_1(),
841 "billing.city" => $order->get_billing_city(),
842 "billing.state" => $order->get_billing_state(),
843 "billing.country" => $order->get_billing_country(),
844 "billing.postcode" => $order->get_billing_postcode(),
845 "shipping.postcode" => $order->get_billing_postcode(),
846 "shipping.street1" => $order->get_billing_address_1(),
847 "shipping.city" => $order->get_billing_city(),
848 "shipping.state" => $order->get_billing_state(),
849 "shipping.country" => $order->get_billing_country(),
850 "shipping.cost" => $shipping_cost,
851 "customParameters[branch_id]" => '1',
852 "customParameters[teller_id]" => '1',
853 "customParameters[device_id]" => '1',
854 "customParameters[plugin]" => 'wordpress',
855 "locale" => get_locale(),
856 ];
857
858 $basicData['body'] = \array_merge($basicData['body'], $data);
859 return $basicData;
860 }
861
862 public function getCheckoutData($order_id)
863 {
864 $order = new WC_Order($order_id);
865
866 $data = $this->buildCheckoutParams($order);
867 $transactionKey = wp_rand(11111111, 99999999);
868 $postBackURL = $order->get_checkout_payment_url(true);
869 $postBackURL .= wp_parse_url($postBackURL, PHP_URL_QUERY) ? '&' : '?';
870 $postBackURL .= 'callback=true';
871 $postBackURL .= "&transaction-key=$transactionKey";
872
873 $data['body']["merchantTransactionId"] = $order_id . "I" . $transactionKey;
874
875
876 if ($this->server_to_server) {
877 $data['body']['shopperResultUrl'] = $postBackURL;
878 $data['body']['paymentBrand'] = $this->brands[0];
879 } else {
880 $data['body']['integrity'] = true;
881 }
882
883
884 // charge 0.00 orders with 0.01 to prevent declined transactions
885 if ($order->get_total() == '0.00') {
886 $data['body']['amount'] = TokenManager::TOKENIZATION_AMOUNT;
887 $data['body']['customParameters[auto_revisal]'] = 'true';
888 }
889
890 // add extra parameters if exists
891 return [
892 "data" => array_replace_recursive($data, $this->setExtraData($order)),
893 "postBackURL" => $postBackURL
894 ];
895 }
896
897 public function prepareCheckout($order_id)
898 {
899 $url = $this->server_to_server
900 ? $this->server_to_server_url
901 : $this->token_url;
902
903 $checkout = $this->getCheckoutData($order_id);
904
905 $response = Http::post($url, $checkout['data']);
906
907 $code = $response['result']['code'] ?? '';
908
909 $isValid = $this->server_to_server
910 ? preg_match('/^(800\.400\.5|100\.400\.500)/', $code)
911 : preg_match('/^(000\.200)/', $code);
912
913 if (!$isValid) {
914 $this->handleError($response);
915 return false;
916 }
917
918 return [
919 'response' => $response,
920 'postBackURL' => $checkout['postBackURL'],
921 ];
922 }
923
924
925 /**
926 * check if all data valid to post {English Characters}
927 * @param array
928 * @return void
929 */
930 function validate_form(array $data)
931 {
932 $errors = [];
933
934 $translations = [
935 'first name' => esc_html__('First Name', 'hyperpay-gateways'),
936 'last name' => esc_html__('Last Name', 'hyperpay-gateways'),
937 'street' => esc_html__('Street', 'hyperpay-gateways'),
938 'state' => esc_html__('State', 'hyperpay-gateways'),
939 'city' => esc_html__('City', 'hyperpay-gateways'),
940 'email' => esc_html__('Email', 'hyperpay-gateways'),
941 ];
942
943 foreach ($data as $key => $field) {
944 if (!preg_match("/^[a-zA-Z0-9-._!`'#%&,:;<>=@{}~\$\(\)\*\+\/\\\?\[\]\^\| +]+$/", $field) || strlen($field) < 2)
945 $errors[$key] = ($translations[$key] ?? esc_html__('Unknown status', 'hyperpay-gateways')) . ' ' . __('format error', 'hyperpay-gateways');
946 }
947
948 if (!preg_match('/^[_a-z0-9-]+(\.[_a-z0-9-]+)*@[a-z0-9-]+(\.[a-z0-9-]+)*(\.[a-z]{2,})$/i', $data['email'])) {
949 $errors['email'] = __('Email format not valid', 'hyperpay-gateways');
950 }
951
952
953 foreach ($errors as $msg) {
954 if (self::has_checkout_block()) {
955 throw new Exception(esc_html($msg));
956 } else {
957 wc_add_notice('<strong>' . $msg . '</strong>', 'error');
958 }
959 }
960
961 // count equal to zero then no errors and it's valid
962 return count($errors);
963 }
964
965 /**
966 *
967 * GET request to transaction report to check if transaction exists or not
968 * @param int
969 * @return array $response
970 *
971 */
972 public function queryTransactionReport(string $merchantTrxId): array
973 {
974 $url = $this->query_url . "&merchantTransactionId=$merchantTrxId";
975 return Http::get($url, ["headers" => ["Authorization" => "Bearer {$this->accessToken}"]]);
976 }
977
978
979 /**
980 *
981 * check the status
982 *
983 * @param array $resultJson
984 * @return string
985 */
986 public function check_status(array $resultJson): string
987 {
988 $status = 'failed';
989 $resultCode = $resultJson['result']['code'] ?? '';
990
991 if (preg_match($this->successCodePattern, $resultCode) || preg_match($this->successManualReviewCodePattern, $resultCode)) {
992 $status = 'success';
993 } elseif (preg_match($this->pendingCodePattern, $resultCode)) {
994 $status = "pending";
995 } elseif (isset($resultJson['card']['bin']) && $resultJson['result']['code'] == '800.300.401' && in_array($resultJson['card']['bin'], $this->blackBins)) {
996 $this->failed_message = __('Sorry! Please select "mada" payment option in order to be able to complete your purchase successfully.', 'hyperpay-gateways');
997 }
998
999 return $status;
1000 }
1001
1002
1003 /**
1004 * handel failed Payments
1005 * @param WC_Order $order
1006 * @param string $message
1007 * @return void
1008 */
1009 public function failed(WC_Order $order, $resultJson)
1010 {
1011
1012 if (isset($_GET["callback"]) && isset($_GET['transaction-key'])) {
1013 // Verify nonce for security (Recommended)
1014 if (!isset($_GET['nonce']) || !wp_verify_nonce(sanitize_text_field(wp_unslash($_GET['nonce'])), 'hyperpay_transaction_nonce')) {
1015 //TODO:
1016 }
1017
1018 $hpOrderId = $order->get_id();
1019 $transactionKey = sanitize_text_field(wp_unslash($_GET['transaction-key']));
1020 $merchantTrxId = $hpOrderId . "I" . $transactionKey;
1021 $queryResponse = $this->queryTransactionReport($merchantTrxId);
1022
1023 if (array_key_exists("payments", $queryResponse)) {
1024 return $this->processQueryResult($queryResponse, $order);
1025 }
1026 }
1027
1028 return $this->handleError($resultJson);
1029 }
1030
1031 public function handleError($resultJson)
1032 {
1033 $order = wc_get_order(wc_clean(get_query_var('order-pay')));
1034
1035 $error_code = $resultJson["result"]["code"];
1036 $error_description = $resultJson["result"]["description"];
1037 $aci_msg = $error_description;
1038 $error_list = [];
1039
1040 if ($error_code == "600.200.500") {
1041 $aci_msg = "configuration error";
1042 Log::write(["error" => $error_description, "response" => $resultJson]);
1043 }
1044
1045 $order->add_order_note("{$this->failed_message} $error_code : $error_description");
1046
1047
1048 $error_list = $this->getExtended($resultJson);
1049
1050 if (empty($error_list)) {
1051 wc_add_notice($this->failed_message, "error");
1052 wc_add_notice($aci_msg, "error");
1053 }
1054
1055 $flat_errors = [];
1056 array_walk_recursive($error_list, function ($item, $key) use (&$flat_errors) {
1057 if ($key === 'error') {
1058 $flat_errors[] = $item;
1059 }
1060 });
1061
1062 foreach ($flat_errors as $error_message) {
1063 wc_add_notice($error_message, "error");
1064 $order->add_order_note("extended description : " . $error_message);
1065 }
1066
1067 $order->update_status("failed");
1068
1069 wc_print_notices();
1070 }
1071
1072 public function getExtended($resultJson)
1073 {
1074 $error_list = [];
1075 if (isset($resultJson["resultDetails"]["ExtendedDescription"])) {
1076 $resultDetails = $resultJson["resultDetails"]["ExtendedDescription"];
1077 if ($this->isJson($resultDetails)) {
1078 $resultDetails = json_decode($resultDetails, true);
1079
1080 if (array_key_exists("details", $resultDetails)) {
1081 $error_list[] = $resultDetails["details"];
1082 } elseif (array_key_exists("message", $resultDetails)) {
1083 $error_list[] = ["error" => $resultDetails['message']];
1084 }
1085 }
1086 }
1087
1088 return $error_list;
1089 }
1090
1091 /**
1092 * check the result of transaction if success of failed
1093 *
1094 * @param array $resultJson
1095 * @param WC_Order $order
1096 * @return void
1097 */
1098 public function processQueryResult(array $resultJson, WC_Order $order)
1099 {
1100 unset($_GET["callback"]);
1101
1102 $payment = end($resultJson["payments"]); // get the last transaction
1103
1104 if (isset($payment["result"]["code"])) {
1105 $status = $this->check_status($payment);
1106 return $this->$status($order, $payment);
1107 }
1108 }
1109
1110 /**
1111 * set customParameters of requested data
1112 * @param WC_Order $order
1113 * @return array
1114 */
1115 public function setExtraData(WC_Order $order): array
1116 {
1117 return [];
1118 }
1119
1120 /**
1121 * update success order
1122 * @param WC_Order $order
1123 * @param array $resultJson
1124 * @return void
1125 */
1126 public function success(WC_Order $order, $resultJson)
1127 {
1128 global $woocommerce;
1129
1130
1131 $woocommerce->cart->empty_cart();
1132 $uniqueId = $resultJson["id"];
1133
1134 //to add action in order details to capture the pre authorization payments
1135 if (array_key_exists('paymentType', $resultJson) && $resultJson['paymentType'] == "PA") {
1136 $order->add_meta_data("is_pre_authorization", true);
1137 $order->add_meta_data("transaction_id", $uniqueId);
1138 $order->add_order_note("pre authorization transaction, need to capture");
1139 $this->order_status = "on-hold";
1140 }
1141
1142 if (array_key_exists("invoice_id", $resultJson["resultDetails"] ?? [])) {
1143 $this->invoice_id = $resultJson["resultDetails"]["invoice_id"];
1144 $order->add_meta_data("invoice_id", $this->invoice_id);
1145 $order->add_order_note("invoice id : " . $this->invoice_id);
1146 }
1147
1148
1149 $order->add_order_note($this->success_message . __("Transaction ID: ", "hyperpay-gateways") . esc_html($uniqueId));
1150 $order->update_status($this->order_status);
1151 $order->payment_complete($uniqueId);
1152 $order->save();
1153
1154 // preform auto-revisal
1155 if (($resultJson['customParameters']['auto_revisal'] ?? null) == 'true') {
1156 $revisal_status = $this->auto_revisal($uniqueId);
1157 $order->add_order_note($revisal_status['message']);
1158 }
1159
1160 do_action('hyperpay_payment_success_' . $this->id, $order, $resultJson);
1161
1162 wp_safe_redirect($this->get_return_url($order));
1163 exit();
1164 }
1165
1166 /**
1167 * update pending order
1168 * @param WC_Order $order
1169 * @param array $resultJson
1170 * @return void
1171 */
1172 public function pending(WC_Order $order, $resultJson)
1173 {
1174 global $woocommerce;
1175
1176
1177 $order->update_status("on-hold");
1178 $order->add_meta_data("gateway_note", __("Transaction is pending confirmation from ", "hyperpay-gateways") . str_replace("hyperpay_", "", $order->get_payment_method()));
1179 $order->save();
1180
1181 $woocommerce->cart->empty_cart();
1182 $uniqueId = $resultJson["id"];
1183
1184 $order->add_order_note("the order waiting gateway confirmation" . __("Transaction ID: ", "hyperpay-gateways") . esc_html($uniqueId));
1185 wp_safe_redirect($this->get_return_url($order));
1186 exit();
1187 }
1188
1189 protected function is_successful_response($response)
1190 {
1191 $result_code = $response['result']['code'] ?? '';
1192 return preg_match($this->successCodePattern, $result_code);
1193 }
1194 }
1195