PluginProbe
VikBooking Hotel Booking Engine & PMS / 1.8.6
VikBooking Hotel Booking Engine & PMS v1.8.6
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 1.7.3 All 36 releases
vikbooking / admin / helpers / widgets / virtual_terminal.php

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

1,352 lines 40.8 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 // ensure a valid payment processor is assigned to the reservation
114 if (!($processor = $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 // tell whether off-session capturing is supported
122 $off_session_capturing = method_exists($processor, 'isOffSessionCaptureSupported') && $processor->isOffSessionCaptureSupported();
123 $off_session_tn_data = $off_session_capturing ? VBOModelReservation::getInstance($booking_info, true)->getOffSessionTransactionData() : [];
124
125 // make sure the permissions are met
126 if ($this->vcm_exists && !JFactory::getUser()->authorise('core.admin', 'com_vikchannelmanager') && !$off_session_tn_data) {
127 // insufficient permissions to handle CC details
128 return [
129 'html' => '<p class="err">' . JText::translate('JERROR_ALERTNOAUTHOR') . '</p>',
130 ];
131 }
132
133 // get the reservation credit card value pairs, if any
134 $cc_value_pairs = VBOModelReservation::getInstance($booking_info, true)->getCardValuePairs();
135
136 // currency code
137 $currency_code = !empty($booking_info['chcurrency']) ? $booking_info['chcurrency'] : VikBooking::getCurrencyName();
138 if (empty($currency_code) || strlen((string)$currency_code) != 3) {
139 // fallback to currency transaction code
140 $currency_code = VikBooking::getCurrencyCodePp();
141 }
142
143 // check known CC values to build the hidden transaction values
144 $known_tn_vals = [];
145 $hidden_tn_vals = [];
146 if (isset($cc_value_pairs['name'])) {
147 $known_tn_vals['name'] = $cc_value_pairs['name'];
148 }
149 if (isset($cc_value_pairs['card_number'])) {
150 $known_tn_vals['card_number'] = $cc_value_pairs['card_number'];
151 }
152 if (isset($cc_value_pairs['expiration_date'])) {
153 $known_tn_vals['expiration_date'] = $cc_value_pairs['expiration_date'];
154 }
155 if (isset($cc_value_pairs['cvv'])) {
156 $known_tn_vals['cvv'] = $cc_value_pairs['cvv'];
157 }
158 foreach ($cc_value_pairs as $key => $value) {
159 if (!isset($known_tn_vals[$key])) {
160 $hidden_tn_vals[$key] = $value;
161 }
162 }
163
164 /**
165 * Consider calculating an outstanding balance in case of previous payments.
166 *
167 * @since 1.16.7 (J) - 1.6.7 (WP)
168 * @since 1.18.0 (J) - 1.8.0 (WP) added support to amount raw for off-session capture.
169 */
170 $default_total = $booking_info['total'];
171 if ($booking_info['totpaid'] > 0 && $booking_info['totpaid'] < $booking_info['total']) {
172 // default to the outstanding balance
173 $default_total = $booking_info['total'] - $booking_info['totpaid'];
174 }
175
176 if (!$booking_info['totpaid'] && $off_session_tn_data && ($off_session_tn_data[0]->amount_raw ?? null)) {
177 // default to the payable amount at the time of payment, unless empty
178 $default_total = ((float) $off_session_tn_data[0]->amount_raw) ?: $booking_info['total'];
179 }
180
181 // tell whether we only have an off-session transaction to capture
182 $only_off_session = $off_session_tn_data && !$cc_value_pairs;
183
184 // start output buffering
185 ob_start();
186
187 ?>
188
189 <div class="vbo-vterminal-cc-container">
190
191 <div class="vbo-vterminal-cc-row-group vbo-vterminal-cc-row-group-amount">
192 <div class="vbo-vterminal-cc-row vbo-vterminal-cc-row-currency">
193 <div class="vbo-vterminal-cc-lbl"><?php echo JText::translate('VBOCPARAMCURRENCY'); ?></div>
194 <div class="vbo-vterminal-cc-val">
195 <input type="text" autocomplete="off" value="<?php echo JHtml::fetch('esc_attr', $currency_code); ?>" data-vt-cc-field="currency" />
196 </div>
197 </div>
198 <div class="vbo-vterminal-cc-row vbo-vterminal-cc-row-amount">
199 <div class="vbo-vterminal-cc-lbl"><?php echo JText::translate('VBO_CC_AMOUNT'); ?></div>
200 <div class="vbo-vterminal-cc-val">
201 <input type="number" value="<?php echo $default_total; ?>" min="0" step="any" data-vt-cc-field="amount" />
202 </div>
203 </div>
204 </div>
205
206 <?php
207 if ($off_session_tn_data) {
208 // when an off-session transaction is available, display the capture button
209 ?>
210 <div class="vbo-vterminal-cc-row-group vbo-vterminal-cc-row-group-submit" data-tn-type="off-session">
211 <div class="vbo-vterminal-cc-row vbo-vterminal-cc-row-submit" data-tn-type="off-session">
212 <div class="vbo-vterminal-cc-val">
213 <button type="button" class="btn vbo-config-btn" onclick="vboWidgetVTerminalOffSessionCharge('<?php echo $wrapper; ?>');"><?php VikBookingIcons::e('credit-card'); ?> <?php echo JText::translate('VBO_CHARGE_AUTHORIZED_CARD'); ?></button>
214 </div>
215 </div>
216 </div>
217 <?php
218 }
219 ?>
220
221 <div class="vbo-vterminal-cc-group-cardwrap"<?php echo $off_session_tn_data ? ' data-has-offsession="1"' : ''; ?>>
222
223 <?php
224 if ($off_session_tn_data) {
225 // add a label for using the card
226 ?>
227 <div class="vbo-vterminal-cc-row-group vbo-vterminal-cc-row-group-usecard">
228 <div class="vbo-vterminal-cc-row vbo-vterminal-cc-row-usecard">
229 <div class="vbo-vterminal-cc-lbl"><?php echo JText::translate('VBO_CREDIT_CARD') . ($only_off_session ? ' - ' . JText::translate('VBMAINPAYMENTSNEW') : ''); ?></div>
230 </div>
231 </div>
232 <?php
233 }
234 ?>
235
236 <div class="vbo-vterminal-cc-row-group vbo-vterminal-cc-row-group-cardholder">
237 <div class="vbo-vterminal-cc-row vbo-vterminal-cc-row-cardholder">
238 <div class="vbo-vterminal-cc-lbl"><?php echo JText::translate('VBISNOMINATIVE'); ?></div>
239 <div class="vbo-vterminal-cc-val">
240 <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" />
241 </div>
242 </div>
243 </div>
244
245 <div class="vbo-vterminal-cc-row-group vbo-vterminal-cc-row-group-ccpan">
246 <div class="vbo-vterminal-cc-row vbo-vterminal-cc-row-ccpan">
247 <div class="vbo-vterminal-cc-lbl"><?php echo JText::translate('VBO_CC_NUMBER'); ?></div>
248 <div class="vbo-vterminal-cc-val vbo-vterminal-cc-val-withlogo">
249 <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" />
250 <span class="vbo-vterminal-cc-type-logo"></span>
251 </div>
252 </div>
253 </div>
254
255 <div class="vbo-vterminal-cc-row-group vbo-vterminal-cc-row-group-ccextra">
256 <div class="vbo-vterminal-cc-row vbo-vterminal-cc-row-ccexpiry">
257 <div class="vbo-vterminal-cc-lbl"><?php echo JText::translate('VBO_CC_EXPIRY_DT'); ?></div>
258 <div class="vbo-vterminal-cc-val">
259 <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" />
260 </div>
261 </div>
262 <div class="vbo-vterminal-cc-row vbo-vterminal-cc-row-cvc">
263 <div class="vbo-vterminal-cc-lbl"><?php echo JText::translate('VBO_CC_CVV'); ?></div>
264 <div class="vbo-vterminal-cc-val">
265 <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" />
266 </div>
267 </div>
268 </div>
269
270 <div class="vbo-vterminal-cc-row-group vbo-vterminal-cc-row-group-submit" data-tn-type="direct-charge">
271 <div class="vbo-vterminal-cc-row vbo-vterminal-cc-row-submit" data-tn-type="direct-charge">
272 <div class="vbo-vterminal-cc-val">
273 <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>
274 </div>
275 </div>
276 </div>
277
278 </div>
279
280 <?php
281 // print hidden transaction values, if any
282 foreach ($hidden_tn_vals as $key => $value) {
283 ?>
284 <input type="hidden" value="<?php echo JHtml::fetch('esc_attr', $value); ?>" data-vt-cc-field="<?php echo JHtml::fetch('esc_attr', $key); ?>" />
285 <?php
286 }
287 ?>
288
289 </div>
290
291 <script type="text/javascript">
292
293 // store the last card type detected
294 var vbo_vt_last_cc_type = '';
295
296 // subscribe to the keyup event for the card number field
297 var vbo_vt_cc_num_f = document.querySelector('#<?php echo $wrapper; ?>').querySelector('[data-vt-cc-field="card_number"]');
298 vbo_vt_cc_num_f.addEventListener('keyup', (e) => {
299 if (!e || !e.target) {
300 return;
301 }
302
303 // the current CC number value
304 var cc_value = e.target.value;
305
306 // invoke the helper class to handle the card number
307 const card = new VBOCreditCard(cc_value);
308
309 // detect card type and format card number
310 var card_type = card.getCardType();
311 var cc_text = card.formatCreditCard(card_type);
312
313 // set formatted CC number value
314 e.target.value = cc_text;
315
316 // handle CC logo
317 if (vbo_vt_last_cc_type != card_type) {
318 var card_logo_uri = card.getCardLogoURI(card_type);
319 var card_logo_pnode = e.target.parentNode;
320 var card_logo_wrap = card_logo_pnode.querySelector('.vbo-vterminal-cc-type-logo');
321 if (!card_type) {
322 // hide CC logo
323 card_logo_wrap.innerHTML = '';
324 card_logo_pnode.classList.remove('vbo-vterminal-cc-type-logo-detected');
325 card_logo_pnode.classList.add('vbo-vterminal-cc-type-logo-unknown');
326 } else if (card_logo_uri) {
327 // set CC logo
328 card_logo_wrap.innerHTML = '<img src="' + card_logo_uri + '" />';
329 card_logo_pnode.classList.remove('vbo-vterminal-cc-type-logo-unknown');
330 card_logo_pnode.classList.add('vbo-vterminal-cc-type-logo-detected');
331 }
332
333 // overwrite value
334 vbo_vt_last_cc_type = card_type;
335 }
336 });
337
338 // subscribe to the keydown and keyup events for the card expiration date field
339 var vbo_vt_cc_exp_f = document.querySelector('#<?php echo $wrapper; ?>').querySelector('[data-vt-cc-field="expiry"]');
340 vbo_vt_cc_exp_f.addEventListener('keydown', (e) => {
341 if (!e || !e.key) {
342 return;
343 }
344
345 if (!isNaN(e.key) || e.altKey || e.ctrlKey || e.metaKey || e.shiftKey) {
346 return true;
347 }
348
349 switch (e.key) {
350 case "ArrowLeft":
351 case "ArrowRight":
352 case "Enter":
353 case "Escape":
354 case "Delete":
355 case "Backspace":
356 case "Tab":
357 case "/":
358 return true;
359 default:
360 event.preventDefault();
361 return false;
362 }
363 });
364
365 vbo_vt_cc_exp_f.addEventListener('keyup', (e) => {
366 if (!e || !e.target) {
367 return;
368 }
369
370 var date = e.target.value;
371
372 if (!date) {
373 return;
374 }
375
376 if (date.length === 2 && date.indexOf('/') < 0) {
377 e.target.value += '/';
378 return;
379 }
380
381 if (date.length > 7) {
382 e.target.value = e.target.value.substr(0, 7);
383 return;
384 }
385 });
386
387 // subscribe to the blur event to make sure the year is full
388 vbo_vt_cc_exp_f.addEventListener('blur', (e) => {
389 if (!e || !e.target) {
390 return;
391 }
392
393 var date = e.target.value;
394
395 if (!date || date.length >= 7 || date.indexOf('/') < 0) {
396 return;
397 }
398
399 var parts = date.split('/');
400
401 if (parts[1].length === 2) {
402 var today = new Date;
403 var year = (today.getFullYear() + '').substr(0, 2);
404
405 e.target.value = parts[0] + '/' + year + parts[1];
406 }
407
408 return;
409 });
410
411 // default state for CC number and expiry input fields
412 setTimeout(() => {
413 if (vbo_vt_cc_num_f.value) {
414 // trigger keyup event to format the current CC and display the logo
415 vbo_vt_cc_num_f.dispatchEvent(new Event('keyup'));
416 }
417 if (vbo_vt_cc_exp_f.value) {
418 // trigger blur event to format the current CC expiration date
419 vbo_vt_cc_exp_f.dispatchEvent(new Event('blur'));
420 }
421 }, 128);
422
423 </script>
424
425 <?php
426
427 // get the HTML buffer
428 $html_content = ob_get_contents();
429 ob_end_clean();
430
431 // return an associative array of values
432 return array(
433 'html' => $html_content,
434 );
435 }
436
437 /**
438 * Custom method for this widget only to debit a credit card.
439 *
440 * @return array|void
441 */
442 public function doDirectCharge()
443 {
444 $wrapper = VikRequest::getString('wrapper', '', 'request');
445 $bid = VikRequest::getInt('bid', 0, 'request');
446 $card = VikRequest::getVar('card', [], 'request');
447
448 if (!$bid || !$card) {
449 // booking ID and CC details are mandatory
450 VBOHttpDocument::getInstance()->close(500, JText::translate('VBPEDITBUSYONE'));
451 }
452
453 // get booking details
454 $booking_info = VikBooking::getBookingInfoFromID($bid);
455
456 if (!$booking_info) {
457 // booking record not found
458 VBOHttpDocument::getInstance()->close(404, JText::translate('VBPEDITBUSYONE'));
459 }
460
461 // get the eligible payment processor by passing the card details
462 $processor = $this->getPaymentProcessor($booking_info, $card);
463
464 if (!$processor) {
465 VBOHttpDocument::getInstance()->close(500, JText::translate('VBO_PAY_PROCESS_NO_DIRECT_CHARGE'));
466 }
467
468 // default transaction response
469 $array_result = [
470 'verified' => 0,
471 ];
472
473 try {
474 // perform the transaction
475 $array_result = $processor->directCharge();
476 } catch (Exception $e) {
477 // set error message
478 $array_result['log'] = sprintf(JText::translate('VBO_CC_TN_ERROR') . " \n%s", $e->getMessage());
479 }
480
481 if ($array_result['verified'] != 1) {
482 // erroneous response
483 if (!empty($array_result['log']) && is_string($array_result['log'])) {
484 VBOHttpDocument::getInstance()->close(500, $array_result['log']);
485 } else {
486 VBOHttpDocument::getInstance()->close(500, 'Operation failed');
487 }
488 }
489
490 // valid transaction response!
491
492 // update booking details
493 $dbo = JFactory::getDbo();
494
495 // get the amount paid
496 $tn_amount = isset($array_result['tot_paid']) ? (float) $array_result['tot_paid'] : null;
497
498 // get the log string, if any
499 $tn_log = !empty($array_result['log']) ? $array_result['log'] : '';
500
501 // update record
502 $upd_record = new stdClass;
503 $upd_record->id = $booking_info['id'];
504 if ($tn_amount) {
505 // update amount paid
506 $upd_record->totpaid = $booking_info['totpaid'] + $tn_amount;
507 // update payable amount (if needed)
508 $new_payable = $booking_info['payable'] - $tn_amount;
509 $new_payable = $new_payable < 0 ? 0 : $new_payable;
510 $upd_record->payable = $new_payable;
511 }
512 if ($tn_log) {
513 $upd_record->paymentlog = $booking_info['paymentlog'] . "\n\n" . date('c') . "\n" . $tn_log;
514 }
515 $upd_record->paymcount = ((int)$booking_info['paymcount'] + 1);
516
517 // update reservation record
518 $dbo->updateObject('#__vikbooking_orders', $upd_record, 'id');
519
520 // payment processor name
521 $pay_process_name = $this->payment_method ? $this->payment_method['name'] : 'CC Direct Charge';
522
523 // handle transaction data to eventually support a later transaction of type refund
524 $tn_data = isset($array_result['transaction']) ? $array_result['transaction'] : null;
525 if ($tn_amount) {
526 // check event data payload to store
527 if (is_array($tn_data)) {
528 // set key
529 $tn_data['amount_paid'] = $tn_amount;
530 } elseif (is_object($tn_data)) {
531 // set property
532 $tn_data->amount_paid = $tn_amount;
533 } elseif (!$tn_data) {
534 // build an array (we add the payment name because we know there is no other transaction data)
535 $tn_data = [
536 'amount_paid' => $tn_amount,
537 'payment_method' => $pay_process_name,
538 ];
539 }
540 }
541
542 /**
543 * Check if the payment processor returned the information about the amount of processing fees.
544 *
545 * @since 1.16.9 (J) - 1.6.9 (WP)
546 */
547 if ($tn_data && isset($array_result['tot_fees']) && $array_result['tot_fees']) {
548 // check event data payload to store
549 if (is_array($tn_data)) {
550 // set key
551 $tn_data['processing_fees'] = (float) $array_result['tot_fees'];
552 } elseif (is_object($tn_data)) {
553 // set property
554 $tn_data->processing_fees = (float) $array_result['tot_fees'];
555 }
556 }
557
558 // current admin-user
559 $now_user = JFactory::getUser();
560
561 // Booking History
562 $ev_descr = JText::translate('VBO_W_VIRTUALTERMINAL_TITLE') . " - {$pay_process_name} ({$now_user->name})";
563 VikBooking::getBookingHistoryInstance()->setBid($booking_info['id'])->setExtraData($tn_data)->store('P' . ($booking_info['paymcount'] > 0 ? 'N' : '0'), $ev_descr);
564
565 return [
566 'success' => 1,
567 'log' => $tn_log,
568 ];
569 }
570
571 /**
572 * Custom method for this widget only to debit a credit card off-session.
573 *
574 * @return ?array
575 *
576 * @since 1.18.0 (J) - 1.8.0 (WP)
577 */
578 public function doOffSessionCapture()
579 {
580 $app = JFactory::getApplication();
581
582 $wrapper = $app->input->getString('wrapper', '');
583 $bid = $app->input->getUInt('bid', 0);
584 $amount = $app->input->getFloat('amount', 0);
585 $currency = $app->input->getString('currency', '');
586
587 if (!$bid || !$amount) {
588 // booking ID and amount are mandatory
589 VBOHttpDocument::getInstance()->close(500, JText::translate('VBPEDITBUSYONE'));
590 }
591
592 // get booking details
593 $booking_info = VikBooking::getBookingInfoFromID($bid);
594
595 if (!$booking_info) {
596 // booking record not found
597 VBOHttpDocument::getInstance()->close(404, JText::translate('VBPEDITBUSYONE'));
598 }
599
600 // collect the previous transaction data list
601 $tn_data = VBOModelReservation::getInstance($booking_info, true)->getOffSessionTransactionData();
602 if (!$tn_data) {
603 // previous transaction data not found
604 VBOHttpDocument::getInstance()->close(400, 'Missing previous transaction data');
605 }
606
607 // set the amount to capture and transaction data before accessing the payment processor
608 $booking_info['total_to_pay'] = $amount;
609 $booking_info['tn_currency'] = $currency;
610 $booking_info['transaction'] = $tn_data[0];
611
612 // get the eligible payment processor
613 $processor = $this->getPaymentProcessor($booking_info);
614
615 if (!$processor) {
616 VBOHttpDocument::getInstance()->close(500, JText::translate('VBO_PAY_PROCESS_NO_DIRECT_CHARGE'));
617 }
618
619 // default transaction response
620 $array_result = [
621 'verified' => 0,
622 ];
623
624 try {
625 // perform the transaction
626 $array_result = $processor->offSessionCapture();
627 } catch (Exception $e) {
628 // set error message
629 $array_result['log'] = sprintf(JText::translate('VBO_CC_TN_ERROR') . " \n%s", $e->getMessage());
630 }
631
632 if ($array_result['verified'] != 1) {
633 // erroneous response
634 if (!empty($array_result['log']) && is_string($array_result['log'])) {
635 VBOHttpDocument::getInstance()->close(500, $array_result['log']);
636 } else {
637 VBOHttpDocument::getInstance()->close(500, 'Operation failed');
638 }
639 }
640
641 // valid transaction response!
642
643 // update booking details
644 $dbo = JFactory::getDbo();
645
646 // get the amount paid
647 $tn_amount = isset($array_result['tot_paid']) ? (float) $array_result['tot_paid'] : null;
648
649 // get the log string, if any
650 $tn_log = !empty($array_result['log']) ? $array_result['log'] : '';
651
652 // update record
653 $upd_record = new stdClass;
654 $upd_record->id = $booking_info['id'];
655 if ($tn_amount) {
656 // update amount paid
657 $upd_record->totpaid = $booking_info['totpaid'] + $tn_amount;
658 // update payable amount (if needed)
659 $new_payable = $booking_info['payable'] - $tn_amount;
660 $new_payable = $new_payable < 0 ? 0 : $new_payable;
661 $upd_record->payable = $new_payable;
662 }
663 if ($tn_log) {
664 $upd_record->paymentlog = $booking_info['paymentlog'] . "\n\n" . date('c') . "\n" . $tn_log;
665 }
666 $upd_record->paymcount = ((int) $booking_info['paymcount'] + 1);
667
668 // update reservation record
669 $dbo->updateObject('#__vikbooking_orders', $upd_record, 'id');
670
671 // payment processor name
672 $pay_process_name = $this->payment_method ? $this->payment_method['name'] : 'CC Manual Charge (Off-Session)';
673
674 // handle transaction data to eventually support a later transaction of type refund
675 $tn_data = isset($array_result['transaction']) ? $array_result['transaction'] : null;
676 if ($tn_amount) {
677 // check event data payload to store
678 if (is_array($tn_data)) {
679 // set key
680 $tn_data['amount_paid'] = $tn_amount;
681 } elseif (is_object($tn_data)) {
682 // set property
683 $tn_data->amount_paid = $tn_amount;
684 } elseif (!$tn_data) {
685 // build an array (we add the payment name because we know there is no other transaction data)
686 $tn_data = [
687 'amount_paid' => $tn_amount,
688 'payment_method' => $pay_process_name,
689 ];
690 }
691 }
692
693 /**
694 * Check if the payment processor returned the information about the amount of processing fees.
695 */
696 if ($tn_data && isset($array_result['tot_fees']) && $array_result['tot_fees']) {
697 // check event data payload to store
698 if (is_array($tn_data)) {
699 // set key
700 $tn_data['processing_fees'] = (float) $array_result['tot_fees'];
701 } elseif (is_object($tn_data)) {
702 // set property
703 $tn_data->processing_fees = (float) $array_result['tot_fees'];
704 }
705 }
706
707 // current admin-user
708 $now_user = JFactory::getUser();
709
710 // Booking History
711 $ev_descr = JText::translate('VBO_W_VIRTUALTERMINAL_TITLE') . " - {$pay_process_name} ({$now_user->name})";
712 VikBooking::getBookingHistoryInstance()->setBid($booking_info['id'])->setExtraData($tn_data)->store('P' . ($booking_info['paymcount'] > 0 ? 'N' : '0'), $ev_descr);
713
714 return [
715 'success' => 1,
716 'log' => $tn_log,
717 ];
718 }
719
720 /**
721 * Preload the necessary assets.
722 *
723 * @return void
724 */
725 public function preload()
726 {
727 // JS lang def
728 JText::script('VBOUPLOADFILEDONE');
729 }
730
731 /**
732 * Main method to invoke the widget. Contents will be loaded
733 * through AJAX requests, not via PHP when the page loads.
734 *
735 * @param ?VBOMultitaskData $data
736 *
737 * @return void
738 */
739 public function render(?VBOMultitaskData $data = null)
740 {
741 // increase widget's instance counter
742 static::$instance_counter++;
743
744 // check whether the widget is being rendered via AJAX when adding it through the customizer
745 $is_ajax = $this->isAjaxRendering();
746
747 // generate a unique ID for the sticky notes wrapper instance
748 $wrapper_instance = !$is_ajax ? static::$instance_counter : rand();
749 $wrapper_id = 'vbo-widget-vterminal-' . $wrapper_instance;
750
751 // get permissions
752 $vbo_auth_bookings = JFactory::getUser()->authorise('core.vbo.bookings', 'com_vikbooking');
753 if (!$vbo_auth_bookings) {
754 // display nothing
755 return;
756 }
757
758 // check multitask data
759 $page_bid = 0;
760 $js_modal_id = '';
761 if ($data) {
762 $is_modal_rendering = $data->isModalRendering();
763 $page_bid = $data->getBookingID() ?: $this->options()->fetchBookingId();
764 if ($is_modal_rendering) {
765 // get modal JS identifier
766 $js_modal_id = $data->getModalJsIdentifier();
767 }
768 }
769
770 if (!$page_bid) {
771 // unable to continue from a page outside the booking details or booking ID set
772 ?>
773 <p class="warn"><?php echo JText::translate('VBPEDITBUSYONE'); ?></p>
774 <?php
775 return;
776 }
777
778 ?>
779 <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; ?>">
780 <div class="vbo-admin-widget-head">
781 <div class="vbo-admin-widget-head-inline">
782 <h4><?php echo $this->widgetIcon; ?> <span><?php echo $this->widgetName; ?></span></h4>
783 </div>
784 </div>
785 <div class="vbo-widget-vterminal-wrap">
786 <div class="vbo-widget-vterminal-inner">
787
788 <div class="vbo-widget-vterminal-form"></div>
789
790 </div>
791 </div>
792 </div>
793 <?php
794
795 if (static::$instance_counter === 0 || $is_ajax) {
796 /**
797 * Print the JS code only once for all instances of this widget.
798 * The real rendering is made through AJAX, not when the page loads.
799 */
800 ?>
801
802 <script type="text/javascript">
803
804 /**
805 * Default icons for status.
806 */
807 var vbo_vt_icon_error = '<?php VikBookingIcons::e('exclamation-circle'); ?>';
808 var vbo_vt_icon_success = '<?php VikBookingIcons::e('check-circle'); ?>';
809
810 /**
811 * Perform the request to load the virtual terminal form.
812 */
813 function vboWidgetVTerminalFormLoad(wrapper, page_bid) {
814 var widget_instance = jQuery('#' + wrapper);
815 if (!widget_instance.length) {
816 return false;
817 }
818
819 // check if multitask data passed a booking ID for the current page
820 var force_bid = 0;
821 if (typeof page_bid !== 'undefined' && page_bid) {
822 force_bid = page_bid;
823 }
824
825 // the widget method to call
826 var call_method = 'loadTerminalForm';
827
828 // make a request to load the bookings calendar
829 VBOCore.doAjax(
830 "<?php echo $this->getExecWidgetAjaxUri(); ?>",
831 {
832 widget_id: "<?php echo $this->getIdentifier(); ?>",
833 call: call_method,
834 return: 1,
835 bid: force_bid,
836 wrapper: wrapper,
837 tmpl: "component"
838 },
839 (response) => {
840 try {
841 var obj_res = typeof response === 'string' ? JSON.parse(response) : response;
842 if (!obj_res.hasOwnProperty(call_method)) {
843 console.error('Unexpected JSON response', obj_res);
844 return false;
845 }
846
847 // replace HTML content
848 widget_instance.find('.vbo-widget-vterminal-form').html(obj_res[call_method]['html']);
849 } catch(err) {
850 console.error('could not parse JSON response', err, response);
851 }
852 },
853 (error) => {
854 // remove the skeleton loading
855 widget_instance.find('.vbo-widget-vterminal-form').find('.vbo-skeleton-loading').remove();
856 // display error message
857 alert(error.responseText);
858 }
859 );
860 }
861
862 /**
863 * Performs the request to charge the given card details.
864 */
865 function vboWidgetVTerminalChargeCard(wrapper) {
866 var widget_instance = jQuery('#' + wrapper);
867 if (!widget_instance.length) {
868 return false;
869 }
870
871 // the trigger button
872 var charge_cmd = widget_instance.find('.vbo-vterminal-cc-row-submit[data-tn-type="direct-charge"]').find('button');
873
874 // disable button
875 charge_cmd.prop('disabled', true);
876
877 if (VBOCore.options.default_loading_body) {
878 // show loading spinner
879 charge_cmd.find('i').replaceWith(VBOCore.options.default_loading_body);
880 }
881
882 // gather all CC fields
883 var cc_fields = {};
884 widget_instance.find('.vbo-widget-vterminal-form').find('[data-vt-cc-field]').each(function() {
885 var cc_f_key = jQuery(this).attr('data-vt-cc-field');
886 cc_fields[cc_f_key] = jQuery(this).val();
887 });
888
889 // get the booking ID for the transaction
890 var force_bid = widget_instance.attr('data-pagebid');
891
892 // the widget method to call
893 var call_method = 'doDirectCharge';
894
895 // make a request to load the bookings calendar
896 VBOCore.doAjax(
897 "<?php echo $this->getExecWidgetAjaxUri(); ?>",
898 {
899 widget_id: "<?php echo $this->getIdentifier(); ?>",
900 call: call_method,
901 return: 1,
902 bid: force_bid,
903 card: cc_fields,
904 wrapper: wrapper,
905 tmpl: "component"
906 },
907 (response) => {
908 try {
909 var obj_res = typeof response === 'string' ? JSON.parse(response) : response;
910 if (!obj_res.hasOwnProperty(call_method)) {
911 console.error('Unexpected JSON response', obj_res);
912 return false;
913 }
914
915 // turn flag on
916 vbo_widget_vt_last_tn = 1;
917
918 // update button status
919 charge_cmd.removeClass('vbo-config-btn').addClass('btn-success').html(vbo_vt_icon_success + ' ' + Joomla.JText._('VBOUPLOADFILEDONE'));
920
921 // check if we need to dismiss the modal widget
922 var js_modal_id = widget_instance.attr('data-modalid');
923 if (js_modal_id) {
924 setTimeout(() => {
925 // dismiss modal widget
926 VBOCore.emitEvent('vbo-dismiss-widget-modal' + js_modal_id);
927 }, 1500);
928 }
929 } catch(err) {
930 console.error('could not parse JSON response', err, response);
931 }
932 },
933 (error) => {
934 // display error message
935 alert(error.responseText);
936 // restore button
937 charge_cmd.prop('disabled', false);
938 charge_cmd.find('i').replaceWith(vbo_vt_icon_error);
939 }
940 );
941 }
942
943 /**
944 * Performs the request to charge a pre-authorized card (off-session).
945 */
946 function vboWidgetVTerminalOffSessionCharge(wrapper) {
947 let widget_instance = jQuery('#' + wrapper);
948 if (!widget_instance.length) {
949 return false;
950 }
951
952 // the trigger button
953 let charge_cmd = widget_instance.find('.vbo-vterminal-cc-row-submit[data-tn-type="off-session"]').find('button');
954
955 // disable button
956 charge_cmd.prop('disabled', true);
957
958 if (VBOCore.options.default_loading_body) {
959 // show loading spinner
960 charge_cmd.find('i').replaceWith(VBOCore.options.default_loading_body);
961 }
962
963 // get the booking ID for the transaction
964 let force_bid = widget_instance.attr('data-pagebid');
965
966 // the widget method to call
967 let call_method = 'doOffSessionCapture';
968
969 // make a request to load the bookings calendar
970 VBOCore.doAjax(
971 "<?php echo $this->getExecWidgetAjaxUri(); ?>",
972 {
973 widget_id: "<?php echo $this->getIdentifier(); ?>",
974 call: call_method,
975 return: 1,
976 bid: force_bid,
977 currency: widget_instance.find('input[data-vt-cc-field="currency"]').val(),
978 amount: widget_instance.find('input[data-vt-cc-field="amount"]').val(),
979 wrapper: wrapper,
980 tmpl: "component"
981 },
982 (response) => {
983 try {
984 let obj_res = typeof response === 'string' ? JSON.parse(response) : response;
985 if (!obj_res.hasOwnProperty(call_method)) {
986 console.error('Unexpected JSON response', obj_res);
987 return false;
988 }
989
990 // turn flag on
991 vbo_widget_vt_last_tn = 1;
992
993 // update button status
994 charge_cmd.removeClass('vbo-config-btn').addClass('btn-success').html(vbo_vt_icon_success + ' ' + Joomla.JText._('VBOUPLOADFILEDONE'));
995
996 // check if we need to dismiss the modal widget
997 let js_modal_id = widget_instance.attr('data-modalid');
998 if (js_modal_id) {
999 setTimeout(() => {
1000 // dismiss modal widget
1001 VBOCore.emitEvent('vbo-dismiss-widget-modal' + js_modal_id);
1002 }, 1500);
1003 }
1004 } catch(err) {
1005 console.error('could not parse JSON response', err, response);
1006 }
1007 },
1008 (error) => {
1009 // display error message
1010 alert(error.responseText);
1011 // restore button
1012 charge_cmd.prop('disabled', false);
1013 charge_cmd.find('i').replaceWith(vbo_vt_icon_error);
1014 }
1015 );
1016 }
1017
1018 /**
1019 * Generate the HTML skeleton loading string to build the form.
1020 */
1021 function vboWidgetVTerminalFormSkeleton(wrapper) {
1022 var widget_instance = jQuery('#' + wrapper);
1023 if (!widget_instance.length) {
1024 return false;
1025 }
1026
1027 var def_loading = '';
1028 def_loading += '<div class="vbo-dashboard-guest-activity vbo-dashboard-guest-activity-skeleton">';
1029 def_loading += ' <div class="vbo-dashboard-guest-activity-avatar">';
1030 def_loading += ' <div class="vbo-skeleton-loading vbo-skeleton-loading-avatar"></div>';
1031 def_loading += ' </div>';
1032 def_loading += ' <div class="vbo-dashboard-guest-activity-content">';
1033 def_loading += ' <div class="vbo-dashboard-guest-activity-content-head">';
1034 def_loading += ' <div class="vbo-skeleton-loading vbo-skeleton-loading-title"></div>';
1035 def_loading += ' </div>';
1036 def_loading += ' <div class="vbo-dashboard-guest-activity-content-subhead">';
1037 def_loading += ' <div class="vbo-skeleton-loading vbo-skeleton-loading-subtitle"></div>';
1038 def_loading += ' </div>';
1039 def_loading += ' <div class="vbo-dashboard-guest-activity-content-info-msg">';
1040 def_loading += ' <div class="vbo-skeleton-loading vbo-skeleton-loading-content"></div>';
1041 def_loading += ' </div>';
1042 def_loading += ' </div>';
1043 def_loading += '</div>';
1044
1045 widget_instance.find('.vbo-widget-vterminal-form').html(def_loading);
1046 }
1047
1048 /**
1049 * Triggers when the multitask panel opens.
1050 */
1051 function vboWidgetVTerminalMultitaskOpen(wrapper) {
1052 var widget_instance = jQuery('#' + wrapper);
1053 if (!widget_instance.length) {
1054 return false;
1055 }
1056
1057 // check if a booking ID was set for this page
1058 var page_bid = widget_instance.attr('data-pagebid');
1059 if (!page_bid || page_bid < 1) {
1060 return false;
1061 }
1062
1063 // show loading skeletons
1064 vboWidgetVTerminalFormSkeleton(wrapper);
1065
1066 // load data by injecting the current booking ID
1067 vboWidgetVTerminalFormLoad(wrapper, page_bid);
1068 }
1069
1070 /**
1071 * Credit Card Detector Class.
1072 */
1073 function VBOCreditCard(card) {
1074 this.set(card);
1075 }
1076
1077 VBOCreditCard.prototype.set = function(card) {
1078 this.cards_logo_uri_base = '<?php echo VBO_ADMIN_URI . 'resources/js_upload/images/'; ?>';
1079 this.card = [];
1080 for (var i = 0; i < card.length; i++) {
1081 var ch = card.charCodeAt(i);
1082 if (ch >= 48 && ch <= 57) {
1083 this.card.push(ch-48);
1084 }
1085 }
1086 }
1087
1088 VBOCreditCard.prototype.get = function() {
1089 return this.card;
1090 }
1091
1092 VBOCreditCard.prototype.isEnoughSpace = function(len) {
1093 return ( this.card.length >= len );
1094 }
1095
1096 VBOCreditCard.prototype.isEmpty = function() {
1097 return !this.isEnoughSpace(1);
1098 }
1099
1100 VBOCreditCard.prototype.isValid = function() {
1101 const type = this.getCardType();
1102
1103 if (type.length && this.card.length == VBOCreditCard.properties[type].size) {
1104 return true;
1105 }
1106
1107 return false;
1108 }
1109
1110 VBOCreditCard.prototype.getNumberToIndex = function(i) {
1111 var n = 0;
1112 var factor = 1;
1113 for (i = i-1 ; i >= 0; i--) {
1114 n += factor * this.card[i];
1115 factor *= 10;
1116 }
1117 return n;
1118 }
1119
1120 VBOCreditCard.prototype.isVisa = function() {
1121 return this.matchBrandRanges([
1122 [4]
1123 ]);
1124 }
1125
1126 VBOCreditCard.prototype.isMasterCard = function() {
1127 return this.matchBrandRanges([
1128 [51, 55],
1129 [2221, 2720]
1130 ]);
1131 }
1132
1133 VBOCreditCard.prototype.isAmericanExpress = function() {
1134 return this.matchBrandRanges([
1135 [34],
1136 [37]
1137 ]);
1138 }
1139
1140 VBOCreditCard.prototype.isDiners = function() {
1141 return this.matchBrandRanges([
1142 [300, 305],
1143 [36],
1144 [38, 39]
1145 ]);
1146 }
1147
1148 VBOCreditCard.prototype.isDiscover = function() {
1149 return this.matchBrandRanges([
1150 [6011],
1151 [65],
1152 [622126, 622925],
1153 [644, 649]
1154 ]);
1155 }
1156
1157 VBOCreditCard.prototype.isJCB = function() {
1158 return this.matchBrandRanges([
1159 [3528, 3589]
1160 ]);
1161 }
1162
1163 VBOCreditCard.prototype.getCardType = function() {
1164 if (this.isVisa()) {
1165 return VBOCreditCard.VISA;
1166 } else if (this.isMasterCard()) {
1167 return VBOCreditCard.MASTERCARD;
1168 } else if (this.isAmericanExpress()) {
1169 return VBOCreditCard.AMERICAN_EXPRESS;
1170 } else if (this.isDiners()) {
1171 return VBOCreditCard.DINERS;
1172 } else if (this.isDiscover()) {
1173 return VBOCreditCard.DISCOVER;
1174 } else if (this.isJCB()) {
1175 return VBOCreditCard.JCB;
1176 }
1177
1178 return '';
1179 }
1180
1181 VBOCreditCard.prototype.getCardLogoURI = function(type) {
1182 if (!type) {
1183 return '';
1184 }
1185
1186 return this.cards_logo_uri_base + type + '.png';
1187 }
1188
1189 VBOCreditCard.prototype.matchBrandRanges = function(ranges) {
1190
1191 for (var i = 0; i < ranges.length; i++) {
1192 var r = ranges[i];
1193
1194 if (r.length == 1) {
1195
1196 if (this.isEnoughSpace((''+r[0]).length) && this.getNumberToIndex((''+r[0]).length) == r[0]) {
1197 return true;
1198 }
1199
1200 } else if (r.length == 2) {
1201
1202 var len = Math.max( (''+r[0]).length, (''+r[1]).length );
1203
1204 if (this.isEnoughSpace(len)) {
1205
1206 var val = this.getNumberToIndex(len);
1207
1208 if (r[0] <= val && val <= r[1]) {
1209 return true;
1210 }
1211
1212 }
1213
1214 }
1215
1216 }
1217
1218 return false;
1219 }
1220
1221 VBOCreditCard.prototype.formatCreditCard = function(card_type) {
1222 if (card_type === undefined) {
1223 card_type = this.getCardType();
1224 }
1225
1226 var blank_spaces = [];
1227 if (card_type.length > 0) {
1228 blank_spaces = VBOCreditCard.properties[card_type]['blank'];
1229 }
1230
1231 var cc_str = '';
1232 for (var i = 0; i < this.card.length; i++) {
1233 cc_str += this.card[i];
1234 if (blank_spaces.indexOf(i+1) != -1) {
1235 cc_str += ' ';
1236 }
1237 }
1238
1239 return cc_str;
1240 }
1241
1242 VBOCreditCard.properties = {
1243 'visa': {
1244 'size': 16,
1245 'blank': [4, 8, 12]
1246 },
1247 'mastercard': {
1248 'size': 16,
1249 'blank': [4, 8, 12]
1250 },
1251 'amex': {
1252 'size': 15,
1253 'blank': [4, 10]
1254 },
1255 'discover': {
1256 'size': 16,
1257 'blank': [4, 8, 12]
1258 },
1259 'diners': {
1260 'size': 14,
1261 'blank': [4, 8, 12]
1262 },
1263 'jcb': {
1264 'size': 16,
1265 'blank': [4, 8, 12]
1266 },
1267 };
1268
1269 VBOCreditCard.VISA = 'visa';
1270 VBOCreditCard.MASTERCARD = 'mastercard';
1271 VBOCreditCard.AMERICAN_EXPRESS = 'amex';
1272 VBOCreditCard.DINERS = 'diners';
1273 VBOCreditCard.DISCOVER = 'discover';
1274 VBOCreditCard.JCB = 'jcb';
1275
1276 </script>
1277 <?php
1278 }
1279 ?>
1280
1281 <script type="text/javascript">
1282
1283 // store the last processed transaction
1284 var vbo_widget_vt_last_tn = null;
1285
1286 jQuery(function() {
1287
1288 // show loading skeletons
1289 vboWidgetVTerminalFormSkeleton('<?php echo $wrapper_id; ?>');
1290
1291 // when document is ready, load terminal form for this widget's instance
1292 vboWidgetVTerminalFormLoad('<?php echo $wrapper_id; ?>', '<?php echo $page_bid; ?>');
1293
1294 // subscribe to the multitask-panel-open event
1295 document.addEventListener(VBOCore.multitask_open_event, function() {
1296 vboWidgetVTerminalMultitaskOpen('<?php echo $wrapper_id; ?>');
1297 });
1298
1299 // subscribe to the multitask-panel-close event to propagate the event for new transaction
1300 document.addEventListener(VBOCore.multitask_close_event, function() {
1301 if (vbo_widget_vt_last_tn) {
1302 // emit the event with data for anyone who is listening to it
1303 VBOCore.emitEvent('vbo_new_payment_transaction', {
1304 tn: vbo_widget_vt_last_tn
1305 });
1306 }
1307 });
1308
1309 <?php
1310 if ($js_modal_id) {
1311 // widget can be dismissed through the modal
1312 ?>
1313 // subscribe to the modal-dismissed event to emit the event for the lastly created booking ID
1314 document.addEventListener(VBOCore.widget_modal_dismissed + '<?php echo $js_modal_id; ?>', function() {
1315 if (vbo_widget_vt_last_tn) {
1316 // emit the event with data for anyone who is listening to it
1317 VBOCore.emitEvent('vbo_new_payment_transaction', {
1318 tn: vbo_widget_vt_last_tn
1319 });
1320 }
1321 });
1322 <?php
1323 }
1324 ?>
1325
1326 });
1327
1328 </script>
1329
1330 <?php
1331 }
1332
1333 /**
1334 * Attempts to invoke the eligible payment processor assigned to the given reservation.
1335 *
1336 * @param array $booking the reservation record as an associative array.
1337 * @param array $card the card details collected through the Virtual Terminal.
1338 *
1339 * @return ?object the payment processor dispatcher instance or null.
1340 */
1341 protected function getPaymentProcessor(array $booking, array $card = [])
1342 {
1343 try {
1344 $processor = VBOModelReservation::getInstance($booking, true)->getPaymentProcessor($card);
1345 } catch (Exception $e) {
1346 $processor = null;
1347 }
1348
1349 return $processor;
1350 }
1351 }
1352