PluginProbe
VikBooking Hotel Booking Engine & PMS / 1.7.0
VikBooking Hotel Booking Engine & PMS v1.7.0
1.8.16 1.8.15 1.8.14 1.8.13 1.8.12 1.8.11 1.8.10 1.8.9 1.8.6 1.8.7 1.8.8 trunk 1.6.0 1.6.1 1.6.2 1.6.3 1.6.4 1.6.5 1.6.6 1.6.7 1.6.8 1.6.9 1.7.0 1.7.1 1.7.2 All 37 releases
vikbooking / admin / helpers / widgets / virtual_terminal.php

virtual_terminal.php in VikBooking Hotel Booking Engine & PMS 1.7.0, at admin/helpers/widgets/virtual_terminal.php

1,112 lines 32.5 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * @package VikBooking
4 * @subpackage com_vikbooking
5 * @author Alessio Gaggii - E4J srl
6 * @copyright Copyright (C) 2023 E4J srl. All rights reserved.
7 * @license GNU General Public License version 2 or later; see LICENSE
8 * @link https://vikwp.com
9 */
10
11 defined('ABSPATH') or die('No script kiddies please!');
12
13 /**
14 * Class handler for admin widget "virtual terminal".
15 *
16 * @since 1.16.4 (J) - 1.6.4 (WP)
17 */
18 class VikBookingAdminWidgetVirtualTerminal extends VikBookingAdminWidget
19 {
20 /**
21 * The instance counter of this widget. Since we do not load individual parameters
22 * for each widget's instance, we use a static counter to determine its settings.
23 *
24 * @var int
25 */
26 protected static $instance_counter = -1;
27
28 /**
29 * Tells whether VCM is installed and updated.
30 *
31 * @var bool
32 */
33 protected $vcm_exists = false;
34
35 /**
36 * The payment processor record loaded.
37 *
38 * @var array
39 */
40 protected $payment_method = [];
41
42 /**
43 * Class constructor will define the widget name and identifier.
44 */
45 public function __construct()
46 {
47 // call parent constructor
48 parent::__construct();
49
50 $this->widgetName = JText::translate('VBO_W_VIRTUALTERMINAL_TITLE');
51 $this->widgetDescr = JText::translate('VBO_W_VIRTUALTERMINAL_DESCR');
52 $this->widgetId = basename(__FILE__, '.php');
53
54 // define widget and icon and style name
55 $this->widgetIcon = '<i class="' . VikBookingIcons::i('credit-card') . '"></i>';
56 $this->widgetStyleName = 'red';
57
58 // whether VCM is available
59 if (is_file(VCM_SITE_PATH . DIRECTORY_SEPARATOR . 'helpers' . DIRECTORY_SEPARATOR . 'lib.vikchannelmanager.php')) {
60 $this->vcm_exists = true;
61 }
62 }
63
64 /**
65 * Custom method for this widget only to load the virtual terminal CC form.
66 * The method is called by the admin controller through an AJAX request.
67 * The visibility should be public, it should not exit the process, and
68 * any content sent to output will be returned to the AJAX response.
69 * In this case we return an array because this method requires "return":1.
70 */
71 public function loadTerminalForm()
72 {
73 $dbo = JFactory::getDbo();
74
75 $wrapper = VikRequest::getString('wrapper', '', 'request');
76 $bid = VikRequest::getString('bid', '', 'request');
77
78 if (!$bid) {
79 // booking ID is mandatory
80 VBOHttpDocument::getInstance()->close(500, JText::translate('VBPEDITBUSYONE'));
81 }
82
83 // get booking details
84 $booking_info = VikBooking::getBookingInfoFromID($bid);
85
86 if (!$booking_info) {
87 // booking record not found, try to see if an OTA reservation ID was given
88 $dbo->setQuery(
89 $dbo->getQuery(true)
90 ->select($dbo->qn('id'))
91 ->from($dbo->qn('#__vikbooking_orders'))
92 ->where($dbo->qn('idorderota') . ' = ' . $dbo->q($bid))
93 ->where($dbo->qn('channel') . ' IS NOT NULL'),
94 0, 1
95 );
96
97 $ota_id = $dbo->loadResult();
98
99 if ($ota_id) {
100 // overwrite booking ID value
101 $bid = $ota_id;
102
103 // get booking details
104 $booking_info = VikBooking::getBookingInfoFromID($bid);
105 }
106
107 if (!$booking_info) {
108 // booking record could not be found
109 VBOHttpDocument::getInstance()->close(404, JText::translate('VBPEDITBUSYONE'));
110 }
111 }
112
113 // check if direct charge is supported by the current payment processor
114 if (!$this->getPaymentProcessor($booking_info)) {
115 // unable to proceed due to unsupported transaction
116 return [
117 'html' => '<p class="err">' . JText::translate('VBO_PAY_PROCESS_NO_DIRECT_CHARGE') . '</p>',
118 ];
119 }
120
121 // make sure the permissions are met
122 if ($this->vcm_exists && !JFactory::getUser()->authorise('core.admin', 'com_vikchannelmanager')) {
123 // insufficient permissions to handle CC details
124 return [
125 'html' => '<p class="err">' . JText::translate('JERROR_ALERTNOAUTHOR') . '</p>',
126 ];
127 }
128
129 // get the reservation credit card value pairs
130 $cc_value_pairs = VBOModelReservation::getInstance($booking_info, true)->getCardValuePairs();
131
132 // currency code
133 $currency_code = !empty($booking_info['chcurrency']) ? $booking_info['chcurrency'] : VikBooking::getCurrencyName();
134 if (empty($currency_code) || strlen((string)$currency_code) != 3) {
135 // fallback to currency transaction code
136 $currency_code = VikBooking::getCurrencyCodePp();
137 }
138
139 // check known CC values to build the hidden transaction values
140 $known_tn_vals = [];
141 $hidden_tn_vals = [];
142 if (isset($cc_value_pairs['name'])) {
143 $known_tn_vals['name'] = $cc_value_pairs['name'];
144 }
145 if (isset($cc_value_pairs['card_number'])) {
146 $known_tn_vals['card_number'] = $cc_value_pairs['card_number'];
147 }
148 if (isset($cc_value_pairs['expiration_date'])) {
149 $known_tn_vals['expiration_date'] = $cc_value_pairs['expiration_date'];
150 }
151 if (isset($cc_value_pairs['cvv'])) {
152 $known_tn_vals['cvv'] = $cc_value_pairs['cvv'];
153 }
154 foreach ($cc_value_pairs as $key => $value) {
155 if (!isset($known_tn_vals[$key])) {
156 $hidden_tn_vals[$key] = $value;
157 }
158 }
159
160 /**
161 * Consider calculating an outstanding balance in case of previous payments.
162 *
163 * @since 1.16.7 (J) - 1.6.7 (WP)
164 */
165 $default_total = $booking_info['total'];
166 if ($booking_info['totpaid'] > 0 && $booking_info['totpaid'] < $booking_info['total']) {
167 // default to the outstanding balance
168 $default_total = $booking_info['total'] - $booking_info['totpaid'];
169 }
170
171 // start output buffering
172 ob_start();
173
174 ?>
175
176 <div class="vbo-vterminal-cc-container">
177
178 <div class="vbo-vterminal-cc-row-group vbo-vterminal-cc-row-group-amount">
179 <div class="vbo-vterminal-cc-row vbo-vterminal-cc-row-currency">
180 <div class="vbo-vterminal-cc-lbl"><?php echo JText::translate('VBOCPARAMCURRENCY'); ?></div>
181 <div class="vbo-vterminal-cc-val">
182 <input type="text" autocomplete="off" value="<?php echo JHtml::fetch('esc_attr', $currency_code); ?>" data-vt-cc-field="currency" />
183 </div>
184 </div>
185 <div class="vbo-vterminal-cc-row vbo-vterminal-cc-row-amount">
186 <div class="vbo-vterminal-cc-lbl"><?php echo JText::translate('VBO_CC_AMOUNT'); ?></div>
187 <div class="vbo-vterminal-cc-val">
188 <input type="number" value="<?php echo $default_total; ?>" min="0" step="any" data-vt-cc-field="amount" />
189 </div>
190 </div>
191 </div>
192
193 <div class="vbo-vterminal-cc-row-group vbo-vterminal-cc-row-group-cardholder">
194 <div class="vbo-vterminal-cc-row vbo-vterminal-cc-row-cardholder">
195 <div class="vbo-vterminal-cc-lbl"><?php echo JText::translate('VBISNOMINATIVE'); ?></div>
196 <div class="vbo-vterminal-cc-val">
197 <input type="text" autocomplete="off" value="<?php echo isset($known_tn_vals['name']) ? JHtml::fetch('esc_attr', $cc_value_pairs['name']) : ''; ?>" data-vt-cc-field="cardholder" />
198 </div>
199 </div>
200 </div>
201
202 <div class="vbo-vterminal-cc-row-group vbo-vterminal-cc-row-group-ccpan">
203 <div class="vbo-vterminal-cc-row vbo-vterminal-cc-row-ccpan">
204 <div class="vbo-vterminal-cc-lbl"><?php echo JText::translate('VBO_CC_NUMBER'); ?></div>
205 <div class="vbo-vterminal-cc-val vbo-vterminal-cc-val-withlogo">
206 <input type="text" autocomplete="off" value="<?php echo isset($known_tn_vals['card_number']) ? JHtml::fetch('esc_attr', $cc_value_pairs['card_number']) : ''; ?>" data-vt-cc-field="card_number" />
207 <span class="vbo-vterminal-cc-type-logo"></span>
208 </div>
209 </div>
210 </div>
211
212 <div class="vbo-vterminal-cc-row-group vbo-vterminal-cc-row-group-ccextra">
213 <div class="vbo-vterminal-cc-row vbo-vterminal-cc-row-ccexpiry">
214 <div class="vbo-vterminal-cc-lbl"><?php echo JText::translate('VBO_CC_EXPIRY_DT'); ?></div>
215 <div class="vbo-vterminal-cc-val">
216 <input type="text" autocomplete="off" placeholder="MM/YYYY" value="<?php echo isset($known_tn_vals['expiration_date']) ? JHtml::fetch('esc_attr', $cc_value_pairs['expiration_date']) : ''; ?>" data-vt-cc-field="expiry" />
217 </div>
218 </div>
219 <div class="vbo-vterminal-cc-row vbo-vterminal-cc-row-cvc">
220 <div class="vbo-vterminal-cc-lbl"><?php echo JText::translate('VBO_CC_CVV'); ?></div>
221 <div class="vbo-vterminal-cc-val">
222 <input type="text" autocomplete="off" value="<?php echo isset($known_tn_vals['cvv']) ? JHtml::fetch('esc_attr', $cc_value_pairs['cvv']) : ''; ?>" data-vt-cc-field="cvv" />
223 </div>
224 </div>
225 </div>
226
227 <div class="vbo-vterminal-cc-row-group vbo-vterminal-cc-row-group-submit">
228 <div class="vbo-vterminal-cc-row vbo-vterminal-cc-row-submit">
229 <div class="vbo-vterminal-cc-val">
230 <button type="button" class="btn vbo-config-btn" onclick="vboWidgetVTerminalChargeCard('<?php echo $wrapper; ?>');"><?php VikBookingIcons::e('credit-card'); ?> <?php echo JText::translate('VBO_CC_DOCHARGE'); ?></button>
231 </div>
232 </div>
233 </div>
234
235 <?php
236 // print hidden transaction values, if any
237 foreach ($hidden_tn_vals as $key => $value) {
238 ?>
239 <input type="hidden" value="<?php echo JHtml::fetch('esc_attr', $value); ?>" data-vt-cc-field="<?php echo JHtml::fetch('esc_attr', $key); ?>" />
240 <?php
241 }
242 ?>
243
244 </div>
245
246 <script type="text/javascript">
247
248 // store the last card type detected
249 var vbo_vt_last_cc_type = '';
250
251 // subscribe to the keyup event for the card number field
252 var vbo_vt_cc_num_f = document.querySelector('#<?php echo $wrapper; ?>').querySelector('[data-vt-cc-field="card_number"]');
253 vbo_vt_cc_num_f.addEventListener('keyup', (e) => {
254 if (!e || !e.target) {
255 return;
256 }
257
258 // the current CC number value
259 var cc_value = e.target.value;
260
261 // invoke the helper class to handle the card number
262 const card = new VBOCreditCard(cc_value);
263
264 // detect card type and format card number
265 var card_type = card.getCardType();
266 var cc_text = card.formatCreditCard(card_type);
267
268 // set formatted CC number value
269 e.target.value = cc_text;
270
271 // handle CC logo
272 if (vbo_vt_last_cc_type != card_type) {
273 var card_logo_uri = card.getCardLogoURI(card_type);
274 var card_logo_pnode = e.target.parentNode;
275 var card_logo_wrap = card_logo_pnode.querySelector('.vbo-vterminal-cc-type-logo');
276 if (!card_type) {
277 // hide CC logo
278 card_logo_wrap.innerHTML = '';
279 card_logo_pnode.classList.remove('vbo-vterminal-cc-type-logo-detected');
280 card_logo_pnode.classList.add('vbo-vterminal-cc-type-logo-unknown');
281 } else if (card_logo_uri) {
282 // set CC logo
283 card_logo_wrap.innerHTML = '<img src="' + card_logo_uri + '" />';
284 card_logo_pnode.classList.remove('vbo-vterminal-cc-type-logo-unknown');
285 card_logo_pnode.classList.add('vbo-vterminal-cc-type-logo-detected');
286 }
287
288 // overwrite value
289 vbo_vt_last_cc_type = card_type;
290 }
291 });
292
293 // subscribe to the keydown and keyup events for the card expiration date field
294 var vbo_vt_cc_exp_f = document.querySelector('#<?php echo $wrapper; ?>').querySelector('[data-vt-cc-field="expiry"]');
295 vbo_vt_cc_exp_f.addEventListener('keydown', (e) => {
296 if (!e || !e.key) {
297 return;
298 }
299
300 if (!isNaN(e.key) || e.altKey || e.ctrlKey || e.metaKey || e.shiftKey) {
301 return true;
302 }
303
304 switch (e.key) {
305 case "ArrowLeft":
306 case "ArrowRight":
307 case "Enter":
308 case "Escape":
309 case "Delete":
310 case "Backspace":
311 case "Tab":
312 case "/":
313 return true;
314 default:
315 event.preventDefault();
316 return false;
317 }
318 });
319
320 vbo_vt_cc_exp_f.addEventListener('keyup', (e) => {
321 if (!e || !e.target) {
322 return;
323 }
324
325 var date = e.target.value;
326
327 if (!date) {
328 return;
329 }
330
331 if (date.length === 2 && date.indexOf('/') < 0) {
332 e.target.value += '/';
333 return;
334 }
335
336 if (date.length > 7) {
337 e.target.value = e.target.value.substr(0, 7);
338 return;
339 }
340 });
341
342 // subscribe to the blur event to make sure the year is full
343 vbo_vt_cc_exp_f.addEventListener('blur', (e) => {
344 if (!e || !e.target) {
345 return;
346 }
347
348 var date = e.target.value;
349
350 if (!date || date.length >= 7 || date.indexOf('/') < 0) {
351 return;
352 }
353
354 var parts = date.split('/');
355
356 if (parts[1].length === 2) {
357 var today = new Date;
358 var year = (today.getFullYear() + '').substr(0, 2);
359
360 e.target.value = parts[0] + '/' + year + parts[1];
361 }
362
363 return;
364 });
365
366 // default state for CC number and expiry input fields
367 setTimeout(() => {
368 if (vbo_vt_cc_num_f.value) {
369 // trigger keyup event to format the current CC and display the logo
370 vbo_vt_cc_num_f.dispatchEvent(new Event('keyup'));
371 }
372 if (vbo_vt_cc_exp_f.value) {
373 // trigger blur event to format the current CC expiration date
374 vbo_vt_cc_exp_f.dispatchEvent(new Event('blur'));
375 }
376 }, 128);
377
378 </script>
379
380 <?php
381
382 // get the HTML buffer
383 $html_content = ob_get_contents();
384 ob_end_clean();
385
386 // return an associative array of values
387 return array(
388 'html' => $html_content,
389 );
390 }
391
392 /**
393 * Custom method for this widget only to debit a credit card.
394 *
395 * @return array|void
396 */
397 public function doDirectCharge()
398 {
399 $wrapper = VikRequest::getString('wrapper', '', 'request');
400 $bid = VikRequest::getInt('bid', 0, 'request');
401 $card = VikRequest::getVar('card', [], 'request');
402
403 if (!$bid || !$card) {
404 // booking ID and CC details are mandatory
405 VBOHttpDocument::getInstance()->close(500, JText::translate('VBPEDITBUSYONE'));
406 }
407
408 // get booking details
409 $booking_info = VikBooking::getBookingInfoFromID($bid);
410
411 if (!$booking_info) {
412 // booking record not found
413 VBOHttpDocument::getInstance()->close(404, JText::translate('VBPEDITBUSYONE'));
414 }
415
416 // get the eligible payment processor by passing the card details
417 $processor = $this->getPaymentProcessor($booking_info, $card);
418
419 if (!$processor) {
420 VBOHttpDocument::getInstance()->close(500, JText::translate('VBO_PAY_PROCESS_NO_DIRECT_CHARGE'));
421 }
422
423 // default transaction response
424 $array_result = [
425 'verified' => 0,
426 ];
427
428 try {
429 // perform the transaction
430 $array_result = $processor->directCharge();
431 } catch (Exception $e) {
432 // set error message
433 $array_result['log'] = sprintf(JText::translate('VBO_CC_TN_ERROR') . " \n%s", $e->getMessage());
434 }
435
436 if ($array_result['verified'] != 1) {
437 // erroneous response
438 if (!empty($array_result['log']) && is_string($array_result['log'])) {
439 VBOHttpDocument::getInstance()->close(500, $array_result['log']);
440 } else {
441 VBOHttpDocument::getInstance()->close(500, 'Operation failed');
442 }
443 }
444
445 // valid transaction response!
446
447 // update booking details
448 $dbo = JFactory::getDbo();
449
450 // get the amount paid
451 $tn_amount = isset($array_result['tot_paid']) ? (float)$array_result['tot_paid'] : null;
452
453 // get the log string, if any
454 $tn_log = !empty($array_result['log']) ? $array_result['log'] : '';
455
456 // update record
457 $upd_record = new stdClass;
458 $upd_record->id = $booking_info['id'];
459 if ($tn_amount) {
460 // update amount paid
461 $upd_record->totpaid = $booking_info['totpaid'] + $tn_amount;
462 // update payable amount (if needed)
463 $new_payable = $booking_info['payable'] - $tn_amount;
464 $new_payable = $new_payable < 0 ? 0 : $new_payable;
465 $upd_record->payable = $new_payable;
466 }
467 if ($tn_log) {
468 $upd_record->paymentlog = $booking_info['paymentlog'] . "\n\n" . date('c') . "\n" . $tn_log;
469 }
470 $upd_record->paymcount = ((int)$booking_info['paymcount'] + 1);
471
472 // update reservation record
473 $dbo->updateObject('#__vikbooking_orders', $upd_record, 'id');
474
475 // payment processor name
476 $pay_process_name = $this->payment_method ? $this->payment_method['name'] : 'CC Direct Charge';
477
478 // handle transaction data to eventually support a later transaction of type refund
479 $tn_data = isset($array_result['transaction']) ? $array_result['transaction'] : null;
480 if ($tn_amount) {
481 // check event data payload to store
482 if (is_array($tn_data)) {
483 // set key
484 $tn_data['amount_paid'] = $tn_amount;
485 } elseif (is_object($tn_data)) {
486 // set property
487 $tn_data->amount_paid = $tn_amount;
488 } elseif (!$tn_data) {
489 // build an array (we add the payment name because we know there is no other transaction data)
490 $tn_data = [
491 'amount_paid' => $tn_amount,
492 'payment_method' => $pay_process_name,
493 ];
494 }
495 }
496
497 /**
498 * Check if the payment processor returned the information about the amount of processing fees.
499 *
500 * @since 1.16.9 (J) - 1.6.9 (WP)
501 */
502 if ($tn_data && isset($array_result['tot_fees']) && $array_result['tot_fees']) {
503 // check event data payload to store
504 if (is_array($tn_data)) {
505 // set key
506 $tn_data['processing_fees'] = (float)$array_result['tot_fees'];
507 } elseif (is_object($tn_data)) {
508 // set property
509 $tn_data->processing_fees = (float)$array_result['tot_fees'];
510 }
511 }
512
513 // current admin-user
514 $now_user = JFactory::getUser();
515
516 // Booking History
517 $ev_descr = JText::translate('VBO_W_VIRTUALTERMINAL_TITLE') . " - {$pay_process_name} ({$now_user->name})";
518 VikBooking::getBookingHistoryInstance()->setBid($booking_info['id'])->setExtraData($tn_data)->store('P' . ($booking_info['paymcount'] > 0 ? 'N' : '0'), $ev_descr);
519
520 return [
521 'success' => 1,
522 'log' => $tn_log,
523 ];
524 }
525
526 /**
527 * Preload the necessary assets.
528 *
529 * @return void
530 */
531 public function preload()
532 {
533 // JS lang def
534 JText::script('VBOUPLOADFILEDONE');
535 }
536
537 /**
538 * Main method to invoke the widget. Contents will be loaded
539 * through AJAX requests, not via PHP when the page loads.
540 *
541 * @param VBOMultitaskData $data
542 *
543 * @return void
544 */
545 public function render(VBOMultitaskData $data = null)
546 {
547 // increase widget's instance counter
548 static::$instance_counter++;
549
550 // check whether the widget is being rendered via AJAX when adding it through the customizer
551 $is_ajax = $this->isAjaxRendering();
552
553 // generate a unique ID for the sticky notes wrapper instance
554 $wrapper_instance = !$is_ajax ? static::$instance_counter : rand();
555 $wrapper_id = 'vbo-widget-vterminal-' . $wrapper_instance;
556
557 // get permissions
558 $vbo_auth_bookings = JFactory::getUser()->authorise('core.vbo.bookings', 'com_vikbooking');
559 if (!$vbo_auth_bookings) {
560 // display nothing
561 return;
562 }
563
564 // check multitask data
565 $page_bid = 0;
566 $js_modal_id = '';
567 if ($data) {
568 $is_modal_rendering = $data->isModalRendering();
569 $page_bid = $data->getBookingID() ?: $this->options()->fetchBookingId();
570 if ($is_modal_rendering) {
571 // get modal JS identifier
572 $js_modal_id = $data->getModalJsIdentifier();
573 }
574 }
575
576 if (!$page_bid) {
577 // unable to continue from a page outside the booking details or booking ID set
578 ?>
579 <p class="warn"><?php echo JText::translate('VBPEDITBUSYONE'); ?></p>
580 <?php
581 return;
582 }
583
584 ?>
585 <div id="<?php echo $wrapper_id; ?>" class="vbo-admin-widget-wrapper" data-instance="<?php echo $wrapper_instance; ?>" data-pagebid="<?php echo $page_bid; ?>" data-modalid="<?php echo $js_modal_id; ?>">
586 <div class="vbo-admin-widget-head">
587 <div class="vbo-admin-widget-head-inline">
588 <h4><?php echo $this->widgetIcon; ?> <span><?php echo $this->widgetName; ?></span></h4>
589 </div>
590 </div>
591 <div class="vbo-widget-vterminal-wrap">
592 <div class="vbo-widget-vterminal-inner">
593
594 <div class="vbo-widget-vterminal-form"></div>
595
596 </div>
597 </div>
598 </div>
599 <?php
600
601 if (static::$instance_counter === 0 || $is_ajax) {
602 /**
603 * Print the JS code only once for all instances of this widget.
604 * The real rendering is made through AJAX, not when the page loads.
605 */
606 ?>
607
608 <script type="text/javascript">
609
610 /**
611 * Default icons for status.
612 */
613 var vbo_vt_icon_error = '<?php VikBookingIcons::e('exclamation-circle'); ?>';
614 var vbo_vt_icon_success = '<?php VikBookingIcons::e('check-circle'); ?>';
615
616 /**
617 * Perform the request to load the virtual terminal form.
618 */
619 function vboWidgetVTerminalFormLoad(wrapper, page_bid) {
620 var widget_instance = jQuery('#' + wrapper);
621 if (!widget_instance.length) {
622 return false;
623 }
624
625 // check if multitask data passed a booking ID for the current page
626 var force_bid = 0;
627 if (typeof page_bid !== 'undefined' && page_bid) {
628 force_bid = page_bid;
629 }
630
631 // the widget method to call
632 var call_method = 'loadTerminalForm';
633
634 // make a request to load the bookings calendar
635 VBOCore.doAjax(
636 "<?php echo $this->getExecWidgetAjaxUri(); ?>",
637 {
638 widget_id: "<?php echo $this->getIdentifier(); ?>",
639 call: call_method,
640 return: 1,
641 bid: force_bid,
642 wrapper: wrapper,
643 tmpl: "component"
644 },
645 (response) => {
646 try {
647 var obj_res = typeof response === 'string' ? JSON.parse(response) : response;
648 if (!obj_res.hasOwnProperty(call_method)) {
649 console.error('Unexpected JSON response', obj_res);
650 return false;
651 }
652
653 // replace HTML content
654 widget_instance.find('.vbo-widget-vterminal-form').html(obj_res[call_method]['html']);
655 } catch(err) {
656 console.error('could not parse JSON response', err, response);
657 }
658 },
659 (error) => {
660 // remove the skeleton loading
661 widget_instance.find('.vbo-widget-vterminal-form').find('.vbo-skeleton-loading').remove();
662 // display error message
663 alert(error.responseText);
664 }
665 );
666 }
667
668 /**
669 * Performs the request to charge the given card details.
670 */
671 function vboWidgetVTerminalChargeCard(wrapper) {
672 var widget_instance = jQuery('#' + wrapper);
673 if (!widget_instance.length) {
674 return false;
675 }
676
677 // the trigger button
678 var charge_cmd = widget_instance.find('.vbo-vterminal-cc-row-submit').find('button');
679
680 // disable button
681 charge_cmd.prop('disabled', true);
682
683 if (VBOCore.options.default_loading_body) {
684 // show loading spinner
685 charge_cmd.find('i').replaceWith(VBOCore.options.default_loading_body);
686 }
687
688 // gather all CC fields
689 var cc_fields = {};
690 widget_instance.find('.vbo-widget-vterminal-form').find('[data-vt-cc-field]').each(function() {
691 var cc_f_key = jQuery(this).attr('data-vt-cc-field');
692 cc_fields[cc_f_key] = jQuery(this).val();
693 });
694
695 // get the booking ID for the transaction
696 var force_bid = widget_instance.attr('data-pagebid');
697
698 // the widget method to call
699 var call_method = 'doDirectCharge';
700
701 // make a request to load the bookings calendar
702 VBOCore.doAjax(
703 "<?php echo $this->getExecWidgetAjaxUri(); ?>",
704 {
705 widget_id: "<?php echo $this->getIdentifier(); ?>",
706 call: call_method,
707 return: 1,
708 bid: force_bid,
709 card: cc_fields,
710 wrapper: wrapper,
711 tmpl: "component"
712 },
713 (response) => {
714 try {
715 var obj_res = typeof response === 'string' ? JSON.parse(response) : response;
716 if (!obj_res.hasOwnProperty(call_method)) {
717 console.error('Unexpected JSON response', obj_res);
718 return false;
719 }
720
721 // turn flag on
722 vbo_widget_vt_last_tn = 1;
723
724 // update button status
725 charge_cmd.removeClass('vbo-config-btn').addClass('btn-success').html(vbo_vt_icon_success + ' ' + Joomla.JText._('VBOUPLOADFILEDONE'));
726
727 // check if we need to dismiss the modal widget
728 var js_modal_id = widget_instance.attr('data-modalid');
729 if (js_modal_id) {
730 setTimeout(() => {
731 // dismiss modal widget
732 VBOCore.emitEvent('vbo-dismiss-widget-modal' + js_modal_id);
733 }, 1500);
734 }
735 } catch(err) {
736 console.error('could not parse JSON response', err, response);
737 }
738 },
739 (error) => {
740 // display error message
741 alert(error.responseText);
742 // restore button
743 charge_cmd.prop('disabled', false);
744 charge_cmd.find('i').replaceWith(vbo_vt_icon_error);
745 }
746 );
747 }
748
749 /**
750 * Generate the HTML skeleton loading string to build the form.
751 */
752 function vboWidgetVTerminalFormSkeleton(wrapper) {
753 var widget_instance = jQuery('#' + wrapper);
754 if (!widget_instance.length) {
755 return false;
756 }
757
758 var def_loading = '';
759 def_loading += '<div class="vbo-dashboard-guest-activity vbo-dashboard-guest-activity-skeleton">';
760 def_loading += ' <div class="vbo-dashboard-guest-activity-avatar">';
761 def_loading += ' <div class="vbo-skeleton-loading vbo-skeleton-loading-avatar"></div>';
762 def_loading += ' </div>';
763 def_loading += ' <div class="vbo-dashboard-guest-activity-content">';
764 def_loading += ' <div class="vbo-dashboard-guest-activity-content-head">';
765 def_loading += ' <div class="vbo-skeleton-loading vbo-skeleton-loading-title"></div>';
766 def_loading += ' </div>';
767 def_loading += ' <div class="vbo-dashboard-guest-activity-content-subhead">';
768 def_loading += ' <div class="vbo-skeleton-loading vbo-skeleton-loading-subtitle"></div>';
769 def_loading += ' </div>';
770 def_loading += ' <div class="vbo-dashboard-guest-activity-content-info-msg">';
771 def_loading += ' <div class="vbo-skeleton-loading vbo-skeleton-loading-content"></div>';
772 def_loading += ' </div>';
773 def_loading += ' </div>';
774 def_loading += '</div>';
775
776 widget_instance.find('.vbo-widget-vterminal-form').html(def_loading);
777 }
778
779 /**
780 * Triggers when the multitask panel opens.
781 */
782 function vboWidgetVTerminalMultitaskOpen(wrapper) {
783 var widget_instance = jQuery('#' + wrapper);
784 if (!widget_instance.length) {
785 return false;
786 }
787
788 // check if a booking ID was set for this page
789 var page_bid = widget_instance.attr('data-pagebid');
790 if (!page_bid || page_bid < 1) {
791 return false;
792 }
793
794 // show loading skeletons
795 vboWidgetVTerminalFormSkeleton(wrapper);
796
797 // load data by injecting the current booking ID
798 vboWidgetVTerminalFormLoad(wrapper, page_bid);
799 }
800
801 /**
802 * Credit Card Detector Class.
803 */
804 function VBOCreditCard(card) {
805 this.set(card);
806 }
807
808 VBOCreditCard.prototype.set = function(card) {
809 this.cards_logo_uri_base = '<?php echo VBO_ADMIN_URI . 'resources/js_upload/images/'; ?>';
810 this.card = [];
811 for (var i = 0; i < card.length; i++) {
812 var ch = card.charCodeAt(i);
813 if (ch >= 48 && ch <= 57) {
814 this.card.push(ch-48);
815 }
816 }
817 }
818
819 VBOCreditCard.prototype.get = function() {
820 return this.card;
821 }
822
823 VBOCreditCard.prototype.isEnoughSpace = function(len) {
824 return ( this.card.length >= len );
825 }
826
827 VBOCreditCard.prototype.isEmpty = function() {
828 return !this.isEnoughSpace(1);
829 }
830
831 VBOCreditCard.prototype.isValid = function() {
832 const type = this.getCardType();
833
834 if (type.length && this.card.length == VBOCreditCard.properties[type].size) {
835 return true;
836 }
837
838 return false;
839 }
840
841 VBOCreditCard.prototype.getNumberToIndex = function(i) {
842 var n = 0;
843 var factor = 1;
844 for (i = i-1 ; i >= 0; i--) {
845 n += factor * this.card[i];
846 factor *= 10;
847 }
848 return n;
849 }
850
851 VBOCreditCard.prototype.isVisa = function() {
852 return this.matchBrandRanges([
853 [4]
854 ]);
855 }
856
857 VBOCreditCard.prototype.isMasterCard = function() {
858 return this.matchBrandRanges([
859 [51, 55],
860 [2221, 2720]
861 ]);
862 }
863
864 VBOCreditCard.prototype.isAmericanExpress = function() {
865 return this.matchBrandRanges([
866 [34],
867 [37]
868 ]);
869 }
870
871 VBOCreditCard.prototype.isDiners = function() {
872 return this.matchBrandRanges([
873 [300, 305],
874 [36],
875 [38, 39]
876 ]);
877 }
878
879 VBOCreditCard.prototype.isDiscover = function() {
880 return this.matchBrandRanges([
881 [6011],
882 [65],
883 [622126, 622925],
884 [644, 649]
885 ]);
886 }
887
888 VBOCreditCard.prototype.isJCB = function() {
889 return this.matchBrandRanges([
890 [3528, 3589]
891 ]);
892 }
893
894 VBOCreditCard.prototype.getCardType = function() {
895 if (this.isVisa()) {
896 return VBOCreditCard.VISA;
897 } else if (this.isMasterCard()) {
898 return VBOCreditCard.MASTERCARD;
899 } else if (this.isAmericanExpress()) {
900 return VBOCreditCard.AMERICAN_EXPRESS;
901 } else if (this.isDiners()) {
902 return VBOCreditCard.DINERS;
903 } else if (this.isDiscover()) {
904 return VBOCreditCard.DISCOVER;
905 } else if (this.isJCB()) {
906 return VBOCreditCard.JCB;
907 }
908
909 return '';
910 }
911
912 VBOCreditCard.prototype.getCardLogoURI = function(type) {
913 if (!type) {
914 return '';
915 }
916
917 return this.cards_logo_uri_base + type + '.png';
918 }
919
920 VBOCreditCard.prototype.matchBrandRanges = function(ranges) {
921
922 for (var i = 0; i < ranges.length; i++) {
923 var r = ranges[i];
924
925 if (r.length == 1) {
926
927 if (this.isEnoughSpace((''+r[0]).length) && this.getNumberToIndex((''+r[0]).length) == r[0]) {
928 return true;
929 }
930
931 } else if (r.length == 2) {
932
933 var len = Math.max( (''+r[0]).length, (''+r[1]).length );
934
935 if (this.isEnoughSpace(len)) {
936
937 var val = this.getNumberToIndex(len);
938
939 if (r[0] <= val && val <= r[1]) {
940 return true;
941 }
942
943 }
944
945 }
946
947 }
948
949 return false;
950 }
951
952 VBOCreditCard.prototype.formatCreditCard = function(card_type) {
953 if (card_type === undefined) {
954 card_type = this.getCardType();
955 }
956
957 var blank_spaces = [];
958 if (card_type.length > 0) {
959 blank_spaces = VBOCreditCard.properties[card_type]['blank'];
960 }
961
962 var cc_str = '';
963 for (var i = 0; i < this.card.length; i++) {
964 cc_str += this.card[i];
965 if (blank_spaces.indexOf(i+1) != -1) {
966 cc_str += ' ';
967 }
968 }
969
970 return cc_str;
971 }
972
973 VBOCreditCard.properties = {
974 'visa': {
975 'size': 16,
976 'blank': [4, 8, 12]
977 },
978 'mastercard': {
979 'size': 16,
980 'blank': [4, 8, 12]
981 },
982 'amex': {
983 'size': 15,
984 'blank': [4, 10]
985 },
986 'discover': {
987 'size': 16,
988 'blank': [4, 8, 12]
989 },
990 'diners': {
991 'size': 14,
992 'blank': [4, 8, 12]
993 },
994 'jcb': {
995 'size': 16,
996 'blank': [4, 8, 12]
997 },
998 };
999
1000 VBOCreditCard.VISA = 'visa';
1001 VBOCreditCard.MASTERCARD = 'mastercard';
1002 VBOCreditCard.AMERICAN_EXPRESS = 'amex';
1003 VBOCreditCard.DINERS = 'diners';
1004 VBOCreditCard.DISCOVER = 'discover';
1005 VBOCreditCard.JCB = 'jcb';
1006
1007 </script>
1008 <?php
1009 }
1010 ?>
1011
1012 <script type="text/javascript">
1013
1014 // store the last processed transaction
1015 var vbo_widget_vt_last_tn = null;
1016
1017 jQuery(function() {
1018
1019 // show loading skeletons
1020 vboWidgetVTerminalFormSkeleton('<?php echo $wrapper_id; ?>');
1021
1022 // when document is ready, load bookings calendar for this widget's instance
1023 vboWidgetVTerminalFormLoad('<?php echo $wrapper_id; ?>', '<?php echo $page_bid; ?>');
1024
1025 // subscribe to the multitask-panel-open event
1026 document.addEventListener(VBOCore.multitask_open_event, function() {
1027 vboWidgetVTerminalMultitaskOpen('<?php echo $wrapper_id; ?>');
1028 });
1029
1030 // subscribe to the multitask-panel-close event to emit the event for the lastly created booking ID
1031 document.addEventListener(VBOCore.multitask_close_event, function() {
1032 if (vbo_widget_vt_last_tn) {
1033 // emit the event with data for anyone who is listening to it
1034 VBOCore.emitEvent('vbo_new_payment_transaction', {
1035 tn: vbo_widget_vt_last_tn
1036 });
1037 }
1038 });
1039
1040 <?php
1041 if ($js_modal_id) {
1042 // widget can be dismissed through the modal
1043 ?>
1044 // subscribe to the modal-dismissed event to emit the event for the lastly created booking ID
1045 document.addEventListener(VBOCore.widget_modal_dismissed + '<?php echo $js_modal_id; ?>', function() {
1046 if (vbo_widget_vt_last_tn) {
1047 // emit the event with data for anyone who is listening to it
1048 VBOCore.emitEvent('vbo_new_payment_transaction', {
1049 tn: vbo_widget_vt_last_tn
1050 });
1051 }
1052 });
1053 <?php
1054 }
1055 ?>
1056
1057 });
1058
1059 </script>
1060
1061 <?php
1062 }
1063
1064 /**
1065 * Attempts to invoke the eligible payment processor assigned to the given reservation.
1066 *
1067 * @param array $booking the reservation record as an associative array.
1068 * @param array $card the card details collected through the Virtual Terminal.
1069 *
1070 * @return object|null the payment processor dispatcher instance or null.
1071 */
1072 protected function getPaymentProcessor(array $booking, array $card = [])
1073 {
1074 $processor = null;
1075 $payment = [];
1076
1077 if (!empty($booking['idpayment'])) {
1078 $payment = VikBooking::getPayment($booking['idpayment']);
1079 }
1080
1081 if ($card) {
1082 // inject CC details for the payment processor
1083 $booking['card'] = $card;
1084 }
1085
1086 if ($payment && VBOPlatformDetection::isWordPress()) {
1087 /**
1088 * @wponly The payment gateway is loaded
1089 * through the apposite dispatcher.
1090 */
1091 JLoader::import('adapter.payment.dispatcher');
1092 $processor = JPaymentDispatcher::getInstance('vikbooking', $payment['file'], $booking, $payment['params']);
1093 } elseif ($payment && VBOPlatformDetection::isJoomla()) {
1094 /**
1095 * @joomlaonly The Payment Factory library will invoke the gateway.
1096 */
1097 require_once VBO_ADMIN_PATH . DIRECTORY_SEPARATOR . 'payments' . DIRECTORY_SEPARATOR . 'libraries' . DIRECTORY_SEPARATOR . 'factory.php';
1098 $processor = VBOPaymentFactory::getPaymentInstance($payment['file'], $booking, $payment['params']);
1099 }
1100
1101 if ($processor && method_exists($processor, 'isDirectChargeSupported') && $processor->isDirectChargeSupported()) {
1102 // set payment method flag loaded
1103 $this->payment_method = $payment;
1104
1105 // return the valid payment processor instance
1106 return $processor;
1107 }
1108
1109 return null;
1110 }
1111 }
1112