Create New Item
Item Type
File
Folder
Item Name
Search file in folder and subfolders...
Are you sure want to rename?
File Manager
/
wp-content
/
plugins
/
vikbooking
/
admin
/
helpers
/
widgets
:
virtual_terminal.php
Advanced Search
Upload
New Item
Settings
Back
Back Up
Advanced Editor
Save
<?php /** * @package VikBooking * @subpackage com_vikbooking * @author Alessio Gaggii - E4J srl * @copyright Copyright (C) 2023 E4J srl. All rights reserved. * @license GNU General Public License version 2 or later; see LICENSE * @link https://vikwp.com */ defined('ABSPATH') or die('No script kiddies please!'); /** * Class handler for admin widget "virtual terminal". * * @since 1.16.4 (J) - 1.6.4 (WP) */ class VikBookingAdminWidgetVirtualTerminal extends VikBookingAdminWidget { /** * The instance counter of this widget. Since we do not load individual parameters * for each widget's instance, we use a static counter to determine its settings. * * @var int */ protected static $instance_counter = -1; /** * Tells whether VCM is installed and updated. * * @var bool */ protected $vcm_exists = false; /** * The payment processor record loaded. * * @var array */ protected $payment_method = []; /** * Class constructor will define the widget name and identifier. */ public function __construct() { // call parent constructor parent::__construct(); $this->widgetName = JText::translate('VBO_W_VIRTUALTERMINAL_TITLE'); $this->widgetDescr = JText::translate('VBO_W_VIRTUALTERMINAL_DESCR'); $this->widgetId = basename(__FILE__, '.php'); // define widget and icon and style name $this->widgetIcon = '<i class="' . VikBookingIcons::i('credit-card') . '"></i>'; $this->widgetStyleName = 'red'; // whether VCM is available if (is_file(VCM_SITE_PATH . DIRECTORY_SEPARATOR . 'helpers' . DIRECTORY_SEPARATOR . 'lib.vikchannelmanager.php')) { $this->vcm_exists = true; } } /** * Custom method for this widget only to load the virtual terminal CC form. * The method is called by the admin controller through an AJAX request. * The visibility should be public, it should not exit the process, and * any content sent to output will be returned to the AJAX response. * In this case we return an array because this method requires "return":1. */ public function loadTerminalForm() { $dbo = JFactory::getDbo(); $wrapper = VikRequest::getString('wrapper', '', 'request'); $bid = VikRequest::getString('bid', '', 'request'); if (!$bid) { // booking ID is mandatory VBOHttpDocument::getInstance()->close(500, JText::translate('VBPEDITBUSYONE')); } // get booking details $booking_info = VikBooking::getBookingInfoFromID($bid); if (!$booking_info) { // booking record not found, try to see if an OTA reservation ID was given $dbo->setQuery( $dbo->getQuery(true) ->select($dbo->qn('id')) ->from($dbo->qn('#__vikbooking_orders')) ->where($dbo->qn('idorderota') . ' = ' . $dbo->q($bid)) ->where($dbo->qn('channel') . ' IS NOT NULL'), 0, 1 ); $ota_id = $dbo->loadResult(); if ($ota_id) { // overwrite booking ID value $bid = $ota_id; // get booking details $booking_info = VikBooking::getBookingInfoFromID($bid); } if (!$booking_info) { // booking record could not be found VBOHttpDocument::getInstance()->close(404, JText::translate('VBPEDITBUSYONE')); } } // ensure a valid payment processor is assigned to the reservation if (!($processor = $this->getPaymentProcessor($booking_info))) { // unable to proceed due to unsupported transaction return [ 'html' => '<p class="err">' . JText::translate('VBO_PAY_PROCESS_NO_DIRECT_CHARGE') . '</p>', ]; } // tell whether off-session capturing is supported $off_session_capturing = method_exists($processor, 'isOffSessionCaptureSupported') && $processor->isOffSessionCaptureSupported(); $off_session_tn_data = $off_session_capturing ? VBOModelReservation::getInstance($booking_info, true)->getOffSessionTransactionData() : []; // make sure the permissions are met if ($this->vcm_exists && !JFactory::getUser()->authorise('core.admin', 'com_vikchannelmanager') && !$off_session_tn_data) { // insufficient permissions to handle CC details return [ 'html' => '<p class="err">' . JText::translate('JERROR_ALERTNOAUTHOR') . '</p>', ]; } // get the reservation credit card value pairs, if any $cc_value_pairs = VBOModelReservation::getInstance($booking_info, true)->getCardValuePairs(); // currency code $currency_code = !empty($booking_info['chcurrency']) ? $booking_info['chcurrency'] : VikBooking::getCurrencyName(); if (empty($currency_code) || strlen((string)$currency_code) != 3) { // fallback to currency transaction code $currency_code = VikBooking::getCurrencyCodePp(); } // check known CC values to build the hidden transaction values $known_tn_vals = []; $hidden_tn_vals = []; if (isset($cc_value_pairs['name'])) { $known_tn_vals['name'] = $cc_value_pairs['name']; } if (isset($cc_value_pairs['card_number'])) { $known_tn_vals['card_number'] = $cc_value_pairs['card_number']; } if (isset($cc_value_pairs['expiration_date'])) { $known_tn_vals['expiration_date'] = $cc_value_pairs['expiration_date']; } if (isset($cc_value_pairs['cvv'])) { $known_tn_vals['cvv'] = $cc_value_pairs['cvv']; } foreach ($cc_value_pairs as $key => $value) { if (!isset($known_tn_vals[$key])) { $hidden_tn_vals[$key] = $value; } } /** * Consider calculating an outstanding balance in case of previous payments. * * @since 1.16.7 (J) - 1.6.7 (WP) * @since 1.18.0 (J) - 1.8.0 (WP) added support to amount raw for off-session capture. */ $default_total = $booking_info['total']; if ($booking_info['totpaid'] > 0 && $booking_info['totpaid'] < $booking_info['total']) { // default to the outstanding balance $default_total = $booking_info['total'] - $booking_info['totpaid']; } if (!$booking_info['totpaid'] && $off_session_tn_data && ($off_session_tn_data[0]->amount_raw ?? null)) { // default to the payable amount at the time of payment, unless empty $default_total = ((float) $off_session_tn_data[0]->amount_raw) ?: $booking_info['total']; } // tell whether we only have an off-session transaction to capture $only_off_session = $off_session_tn_data && !$cc_value_pairs; // start output buffering ob_start(); ?> <div class="vbo-vterminal-cc-container"> <div class="vbo-vterminal-cc-row-group vbo-vterminal-cc-row-group-amount"> <div class="vbo-vterminal-cc-row vbo-vterminal-cc-row-currency"> <div class="vbo-vterminal-cc-lbl"><?php echo JText::translate('VBOCPARAMCURRENCY'); ?></div> <div class="vbo-vterminal-cc-val"> <input type="text" autocomplete="off" value="<?php echo JHtml::fetch('esc_attr', $currency_code); ?>" data-vt-cc-field="currency" /> </div> </div> <div class="vbo-vterminal-cc-row vbo-vterminal-cc-row-amount"> <div class="vbo-vterminal-cc-lbl"><?php echo JText::translate('VBO_CC_AMOUNT'); ?></div> <div class="vbo-vterminal-cc-val"> <input type="number" value="<?php echo $default_total; ?>" min="0" step="any" data-vt-cc-field="amount" /> </div> </div> </div> <?php if ($off_session_tn_data) { // when an off-session transaction is available, display the capture button ?> <div class="vbo-vterminal-cc-row-group vbo-vterminal-cc-row-group-submit" data-tn-type="off-session"> <div class="vbo-vterminal-cc-row vbo-vterminal-cc-row-submit" data-tn-type="off-session"> <div class="vbo-vterminal-cc-val"> <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> </div> </div> </div> <?php } ?> <div class="vbo-vterminal-cc-group-cardwrap"<?php echo $off_session_tn_data ? ' data-has-offsession="1"' : ''; ?>> <?php if ($off_session_tn_data) { // add a label for using the card ?> <div class="vbo-vterminal-cc-row-group vbo-vterminal-cc-row-group-usecard"> <div class="vbo-vterminal-cc-row vbo-vterminal-cc-row-usecard"> <div class="vbo-vterminal-cc-lbl"><?php echo JText::translate('VBO_CREDIT_CARD') . ($only_off_session ? ' - ' . JText::translate('VBMAINPAYMENTSNEW') : ''); ?></div> </div> </div> <?php } ?> <div class="vbo-vterminal-cc-row-group vbo-vterminal-cc-row-group-cardholder"> <div class="vbo-vterminal-cc-row vbo-vterminal-cc-row-cardholder"> <div class="vbo-vterminal-cc-lbl"><?php echo JText::translate('VBISNOMINATIVE'); ?></div> <div class="vbo-vterminal-cc-val"> <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" /> </div> </div> </div> <div class="vbo-vterminal-cc-row-group vbo-vterminal-cc-row-group-ccpan"> <div class="vbo-vterminal-cc-row vbo-vterminal-cc-row-ccpan"> <div class="vbo-vterminal-cc-lbl"><?php echo JText::translate('VBO_CC_NUMBER'); ?></div> <div class="vbo-vterminal-cc-val vbo-vterminal-cc-val-withlogo"> <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" /> <span class="vbo-vterminal-cc-type-logo"></span> </div> </div> </div> <div class="vbo-vterminal-cc-row-group vbo-vterminal-cc-row-group-ccextra"> <div class="vbo-vterminal-cc-row vbo-vterminal-cc-row-ccexpiry"> <div class="vbo-vterminal-cc-lbl"><?php echo JText::translate('VBO_CC_EXPIRY_DT'); ?></div> <div class="vbo-vterminal-cc-val"> <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" /> </div> </div> <div class="vbo-vterminal-cc-row vbo-vterminal-cc-row-cvc"> <div class="vbo-vterminal-cc-lbl"><?php echo JText::translate('VBO_CC_CVV'); ?></div> <div class="vbo-vterminal-cc-val"> <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" /> </div> </div> </div> <div class="vbo-vterminal-cc-row-group vbo-vterminal-cc-row-group-submit" data-tn-type="direct-charge"> <div class="vbo-vterminal-cc-row vbo-vterminal-cc-row-submit" data-tn-type="direct-charge"> <div class="vbo-vterminal-cc-val"> <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> </div> </div> </div> </div> <?php // print hidden transaction values, if any foreach ($hidden_tn_vals as $key => $value) { ?> <input type="hidden" value="<?php echo JHtml::fetch('esc_attr', $value); ?>" data-vt-cc-field="<?php echo JHtml::fetch('esc_attr', $key); ?>" /> <?php } ?> </div> <script type="text/javascript"> // store the last card type detected var vbo_vt_last_cc_type = ''; // subscribe to the keyup event for the card number field var vbo_vt_cc_num_f = document.querySelector('#<?php echo $wrapper; ?>').querySelector('[data-vt-cc-field="card_number"]'); vbo_vt_cc_num_f.addEventListener('keyup', (e) => { if (!e || !e.target) { return; } // the current CC number value var cc_value = e.target.value; // invoke the helper class to handle the card number const card = new VBOCreditCard(cc_value); // detect card type and format card number var card_type = card.getCardType(); var cc_text = card.formatCreditCard(card_type); // set formatted CC number value e.target.value = cc_text; // handle CC logo if (vbo_vt_last_cc_type != card_type) { var card_logo_uri = card.getCardLogoURI(card_type); var card_logo_pnode = e.target.parentNode; var card_logo_wrap = card_logo_pnode.querySelector('.vbo-vterminal-cc-type-logo'); if (!card_type) { // hide CC logo card_logo_wrap.innerHTML = ''; card_logo_pnode.classList.remove('vbo-vterminal-cc-type-logo-detected'); card_logo_pnode.classList.add('vbo-vterminal-cc-type-logo-unknown'); } else if (card_logo_uri) { // set CC logo card_logo_wrap.innerHTML = '<img src="' + card_logo_uri + '" />'; card_logo_pnode.classList.remove('vbo-vterminal-cc-type-logo-unknown'); card_logo_pnode.classList.add('vbo-vterminal-cc-type-logo-detected'); } // overwrite value vbo_vt_last_cc_type = card_type; } }); // subscribe to the keydown and keyup events for the card expiration date field var vbo_vt_cc_exp_f = document.querySelector('#<?php echo $wrapper; ?>').querySelector('[data-vt-cc-field="expiry"]'); vbo_vt_cc_exp_f.addEventListener('keydown', (e) => { if (!e || !e.key) { return; } if (!isNaN(e.key) || e.altKey || e.ctrlKey || e.metaKey || e.shiftKey) { return true; } switch (e.key) { case "ArrowLeft": case "ArrowRight": case "Enter": case "Escape": case "Delete": case "Backspace": case "Tab": case "/": return true; default: event.preventDefault(); return false; } }); vbo_vt_cc_exp_f.addEventListener('keyup', (e) => { if (!e || !e.target) { return; } var date = e.target.value; if (!date) { return; } if (date.length === 2 && date.indexOf('/') < 0) { e.target.value += '/'; return; } if (date.length > 7) { e.target.value = e.target.value.substr(0, 7); return; } }); // subscribe to the blur event to make sure the year is full vbo_vt_cc_exp_f.addEventListener('blur', (e) => { if (!e || !e.target) { return; } var date = e.target.value; if (!date || date.length >= 7 || date.indexOf('/') < 0) { return; } var parts = date.split('/'); if (parts[1].length === 2) { var today = new Date; var year = (today.getFullYear() + '').substr(0, 2); e.target.value = parts[0] + '/' + year + parts[1]; } return; }); // default state for CC number and expiry input fields setTimeout(() => { if (vbo_vt_cc_num_f.value) { // trigger keyup event to format the current CC and display the logo vbo_vt_cc_num_f.dispatchEvent(new Event('keyup')); } if (vbo_vt_cc_exp_f.value) { // trigger blur event to format the current CC expiration date vbo_vt_cc_exp_f.dispatchEvent(new Event('blur')); } }, 128); </script> <?php // get the HTML buffer $html_content = ob_get_contents(); ob_end_clean(); // return an associative array of values return array( 'html' => $html_content, ); } /** * Custom method for this widget only to debit a credit card. * * @return array|void */ public function doDirectCharge() { $wrapper = VikRequest::getString('wrapper', '', 'request'); $bid = VikRequest::getInt('bid', 0, 'request'); $card = VikRequest::getVar('card', [], 'request'); if (!$bid || !$card) { // booking ID and CC details are mandatory VBOHttpDocument::getInstance()->close(500, JText::translate('VBPEDITBUSYONE')); } // get booking details $booking_info = VikBooking::getBookingInfoFromID($bid); if (!$booking_info) { // booking record not found VBOHttpDocument::getInstance()->close(404, JText::translate('VBPEDITBUSYONE')); } // get the eligible payment processor by passing the card details $processor = $this->getPaymentProcessor($booking_info, $card); if (!$processor) { VBOHttpDocument::getInstance()->close(500, JText::translate('VBO_PAY_PROCESS_NO_DIRECT_CHARGE')); } // default transaction response $array_result = [ 'verified' => 0, ]; try { // perform the transaction $array_result = $processor->directCharge(); } catch (Exception $e) { // set error message $array_result['log'] = sprintf(JText::translate('VBO_CC_TN_ERROR') . " \n%s", $e->getMessage()); } if ($array_result['verified'] != 1) { // erroneous response if (!empty($array_result['log']) && is_string($array_result['log'])) { VBOHttpDocument::getInstance()->close(500, $array_result['log']); } else { VBOHttpDocument::getInstance()->close(500, 'Operation failed'); } } // valid transaction response! // update booking details $dbo = JFactory::getDbo(); // get the amount paid $tn_amount = isset($array_result['tot_paid']) ? (float) $array_result['tot_paid'] : null; // get the log string, if any $tn_log = !empty($array_result['log']) ? $array_result['log'] : ''; // update record $upd_record = new stdClass; $upd_record->id = $booking_info['id']; if ($tn_amount) { // update amount paid $upd_record->totpaid = $booking_info['totpaid'] + $tn_amount; // update payable amount (if needed) $new_payable = $booking_info['payable'] - $tn_amount; $new_payable = $new_payable < 0 ? 0 : $new_payable; $upd_record->payable = $new_payable; } if ($tn_log) { $upd_record->paymentlog = $booking_info['paymentlog'] . "\n\n" . date('c') . "\n" . $tn_log; } $upd_record->paymcount = ((int)$booking_info['paymcount'] + 1); // update reservation record $dbo->updateObject('#__vikbooking_orders', $upd_record, 'id'); // payment processor name $pay_process_name = $this->payment_method ? $this->payment_method['name'] : 'CC Direct Charge'; // handle transaction data to eventually support a later transaction of type refund $tn_data = isset($array_result['transaction']) ? $array_result['transaction'] : null; if ($tn_amount) { // check event data payload to store if (is_array($tn_data)) { // set key $tn_data['amount_paid'] = $tn_amount; } elseif (is_object($tn_data)) { // set property $tn_data->amount_paid = $tn_amount; } elseif (!$tn_data) { // build an array (we add the payment name because we know there is no other transaction data) $tn_data = [ 'amount_paid' => $tn_amount, 'payment_method' => $pay_process_name, ]; } } /** * Check if the payment processor returned the information about the amount of processing fees. * * @since 1.16.9 (J) - 1.6.9 (WP) */ if ($tn_data && isset($array_result['tot_fees']) && $array_result['tot_fees']) { // check event data payload to store if (is_array($tn_data)) { // set key $tn_data['processing_fees'] = (float) $array_result['tot_fees']; } elseif (is_object($tn_data)) { // set property $tn_data->processing_fees = (float) $array_result['tot_fees']; } } // current admin-user $now_user = JFactory::getUser(); // Booking History $ev_descr = JText::translate('VBO_W_VIRTUALTERMINAL_TITLE') . " - {$pay_process_name} ({$now_user->name})"; VikBooking::getBookingHistoryInstance()->setBid($booking_info['id'])->setExtraData($tn_data)->store('P' . ($booking_info['paymcount'] > 0 ? 'N' : '0'), $ev_descr); return [ 'success' => 1, 'log' => $tn_log, ]; } /** * Custom method for this widget only to debit a credit card off-session. * * @return ?array * * @since 1.18.0 (J) - 1.8.0 (WP) */ public function doOffSessionCapture() { $app = JFactory::getApplication(); $wrapper = $app->input->getString('wrapper', ''); $bid = $app->input->getUInt('bid', 0); $amount = $app->input->getFloat('amount', 0); $currency = $app->input->getString('currency', ''); if (!$bid || !$amount) { // booking ID and amount are mandatory VBOHttpDocument::getInstance()->close(500, JText::translate('VBPEDITBUSYONE')); } // get booking details $booking_info = VikBooking::getBookingInfoFromID($bid); if (!$booking_info) { // booking record not found VBOHttpDocument::getInstance()->close(404, JText::translate('VBPEDITBUSYONE')); } // collect the previous transaction data list $tn_data = VBOModelReservation::getInstance($booking_info, true)->getOffSessionTransactionData(); if (!$tn_data) { // previous transaction data not found VBOHttpDocument::getInstance()->close(400, 'Missing previous transaction data'); } // set the amount to capture and transaction data before accessing the payment processor $booking_info['total_to_pay'] = $amount; $booking_info['tn_currency'] = $currency; $booking_info['transaction'] = $tn_data[0]; // get the eligible payment processor $processor = $this->getPaymentProcessor($booking_info); if (!$processor) { VBOHttpDocument::getInstance()->close(500, JText::translate('VBO_PAY_PROCESS_NO_DIRECT_CHARGE')); } // default transaction response $array_result = [ 'verified' => 0, ]; try { // perform the transaction $array_result = $processor->offSessionCapture(); } catch (Exception $e) { // set error message $array_result['log'] = sprintf(JText::translate('VBO_CC_TN_ERROR') . " \n%s", $e->getMessage()); } if ($array_result['verified'] != 1) { // erroneous response if (!empty($array_result['log']) && is_string($array_result['log'])) { VBOHttpDocument::getInstance()->close(500, $array_result['log']); } else { VBOHttpDocument::getInstance()->close(500, 'Operation failed'); } } // valid transaction response! // update booking details $dbo = JFactory::getDbo(); // get the amount paid $tn_amount = isset($array_result['tot_paid']) ? (float) $array_result['tot_paid'] : null; // get the log string, if any $tn_log = !empty($array_result['log']) ? $array_result['log'] : ''; // update record $upd_record = new stdClass; $upd_record->id = $booking_info['id']; if ($tn_amount) { // update amount paid $upd_record->totpaid = $booking_info['totpaid'] + $tn_amount; // update payable amount (if needed) $new_payable = $booking_info['payable'] - $tn_amount; $new_payable = $new_payable < 0 ? 0 : $new_payable; $upd_record->payable = $new_payable; } if ($tn_log) { $upd_record->paymentlog = $booking_info['paymentlog'] . "\n\n" . date('c') . "\n" . $tn_log; } $upd_record->paymcount = ((int) $booking_info['paymcount'] + 1); // update reservation record $dbo->updateObject('#__vikbooking_orders', $upd_record, 'id'); // payment processor name $pay_process_name = $this->payment_method ? $this->payment_method['name'] : 'CC Manual Charge (Off-Session)'; // handle transaction data to eventually support a later transaction of type refund $tn_data = isset($array_result['transaction']) ? $array_result['transaction'] : null; if ($tn_amount) { // check event data payload to store if (is_array($tn_data)) { // set key $tn_data['amount_paid'] = $tn_amount; } elseif (is_object($tn_data)) { // set property $tn_data->amount_paid = $tn_amount; } elseif (!$tn_data) { // build an array (we add the payment name because we know there is no other transaction data) $tn_data = [ 'amount_paid' => $tn_amount, 'payment_method' => $pay_process_name, ]; } } /** * Check if the payment processor returned the information about the amount of processing fees. */ if ($tn_data && isset($array_result['tot_fees']) && $array_result['tot_fees']) { // check event data payload to store if (is_array($tn_data)) { // set key $tn_data['processing_fees'] = (float) $array_result['tot_fees']; } elseif (is_object($tn_data)) { // set property $tn_data->processing_fees = (float) $array_result['tot_fees']; } } // current admin-user $now_user = JFactory::getUser(); // Booking History $ev_descr = JText::translate('VBO_W_VIRTUALTERMINAL_TITLE') . " - {$pay_process_name} ({$now_user->name})"; VikBooking::getBookingHistoryInstance()->setBid($booking_info['id'])->setExtraData($tn_data)->store('P' . ($booking_info['paymcount'] > 0 ? 'N' : '0'), $ev_descr); return [ 'success' => 1, 'log' => $tn_log, ]; } /** * Preload the necessary assets. * * @return void */ public function preload() { // JS lang def JText::script('VBOUPLOADFILEDONE'); } /** * Main method to invoke the widget. Contents will be loaded * through AJAX requests, not via PHP when the page loads. * * @param ?VBOMultitaskData $data * * @return void */ public function render(?VBOMultitaskData $data = null) { // increase widget's instance counter static::$instance_counter++; // check whether the widget is being rendered via AJAX when adding it through the customizer $is_ajax = $this->isAjaxRendering(); // generate a unique ID for the sticky notes wrapper instance $wrapper_instance = !$is_ajax ? static::$instance_counter : rand(); $wrapper_id = 'vbo-widget-vterminal-' . $wrapper_instance; // get permissions $vbo_auth_bookings = JFactory::getUser()->authorise('core.vbo.bookings', 'com_vikbooking'); if (!$vbo_auth_bookings) { // display nothing return; } // check multitask data $page_bid = 0; $js_modal_id = ''; if ($data) { $is_modal_rendering = $data->isModalRendering(); $page_bid = $data->getBookingID() ?: $this->options()->fetchBookingId(); if ($is_modal_rendering) { // get modal JS identifier $js_modal_id = $data->getModalJsIdentifier(); } } if (!$page_bid) { // unable to continue from a page outside the booking details or booking ID set ?> <p class="warn"><?php echo JText::translate('VBPEDITBUSYONE'); ?></p> <?php return; } ?> <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; ?>"> <div class="vbo-admin-widget-head"> <div class="vbo-admin-widget-head-inline"> <h4><?php echo $this->widgetIcon; ?> <span><?php echo $this->widgetName; ?></span></h4> </div> </div> <div class="vbo-widget-vterminal-wrap"> <div class="vbo-widget-vterminal-inner"> <div class="vbo-widget-vterminal-form"></div> </div> </div> </div> <?php if (static::$instance_counter === 0 || $is_ajax) { /** * Print the JS code only once for all instances of this widget. * The real rendering is made through AJAX, not when the page loads. */ ?> <script type="text/javascript"> /** * Default icons for status. */ var vbo_vt_icon_error = '<?php VikBookingIcons::e('exclamation-circle'); ?>'; var vbo_vt_icon_success = '<?php VikBookingIcons::e('check-circle'); ?>'; /** * Perform the request to load the virtual terminal form. */ function vboWidgetVTerminalFormLoad(wrapper, page_bid) { var widget_instance = jQuery('#' + wrapper); if (!widget_instance.length) { return false; } // check if multitask data passed a booking ID for the current page var force_bid = 0; if (typeof page_bid !== 'undefined' && page_bid) { force_bid = page_bid; } // the widget method to call var call_method = 'loadTerminalForm'; // make a request to load the bookings calendar VBOCore.doAjax( "<?php echo $this->getExecWidgetAjaxUri(); ?>", { widget_id: "<?php echo $this->getIdentifier(); ?>", call: call_method, return: 1, bid: force_bid, wrapper: wrapper, tmpl: "component" }, (response) => { try { var obj_res = typeof response === 'string' ? JSON.parse(response) : response; if (!obj_res.hasOwnProperty(call_method)) { console.error('Unexpected JSON response', obj_res); return false; } // replace HTML content widget_instance.find('.vbo-widget-vterminal-form').html(obj_res[call_method]['html']); } catch(err) { console.error('could not parse JSON response', err, response); } }, (error) => { // remove the skeleton loading widget_instance.find('.vbo-widget-vterminal-form').find('.vbo-skeleton-loading').remove(); // display error message alert(error.responseText); } ); } /** * Performs the request to charge the given card details. */ function vboWidgetVTerminalChargeCard(wrapper) { var widget_instance = jQuery('#' + wrapper); if (!widget_instance.length) { return false; } // the trigger button var charge_cmd = widget_instance.find('.vbo-vterminal-cc-row-submit[data-tn-type="direct-charge"]').find('button'); // disable button charge_cmd.prop('disabled', true); if (VBOCore.options.default_loading_body) { // show loading spinner charge_cmd.find('i').replaceWith(VBOCore.options.default_loading_body); } // gather all CC fields var cc_fields = {}; widget_instance.find('.vbo-widget-vterminal-form').find('[data-vt-cc-field]').each(function() { var cc_f_key = jQuery(this).attr('data-vt-cc-field'); cc_fields[cc_f_key] = jQuery(this).val(); }); // get the booking ID for the transaction var force_bid = widget_instance.attr('data-pagebid'); // the widget method to call var call_method = 'doDirectCharge'; // make a request to load the bookings calendar VBOCore.doAjax( "<?php echo $this->getExecWidgetAjaxUri(); ?>", { widget_id: "<?php echo $this->getIdentifier(); ?>", call: call_method, return: 1, bid: force_bid, card: cc_fields, wrapper: wrapper, tmpl: "component" }, (response) => { try { var obj_res = typeof response === 'string' ? JSON.parse(response) : response; if (!obj_res.hasOwnProperty(call_method)) { console.error('Unexpected JSON response', obj_res); return false; } // turn flag on vbo_widget_vt_last_tn = 1; // update button status charge_cmd.removeClass('vbo-config-btn').addClass('btn-success').html(vbo_vt_icon_success + ' ' + Joomla.JText._('VBOUPLOADFILEDONE')); // check if we need to dismiss the modal widget var js_modal_id = widget_instance.attr('data-modalid'); if (js_modal_id) { setTimeout(() => { // dismiss modal widget VBOCore.emitEvent('vbo-dismiss-widget-modal' + js_modal_id); }, 1500); } } catch(err) { console.error('could not parse JSON response', err, response); } }, (error) => { // display error message alert(error.responseText); // restore button charge_cmd.prop('disabled', false); charge_cmd.find('i').replaceWith(vbo_vt_icon_error); } ); } /** * Performs the request to charge a pre-authorized card (off-session). */ function vboWidgetVTerminalOffSessionCharge(wrapper) { let widget_instance = jQuery('#' + wrapper); if (!widget_instance.length) { return false; } // the trigger button let charge_cmd = widget_instance.find('.vbo-vterminal-cc-row-submit[data-tn-type="off-session"]').find('button'); // disable button charge_cmd.prop('disabled', true); if (VBOCore.options.default_loading_body) { // show loading spinner charge_cmd.find('i').replaceWith(VBOCore.options.default_loading_body); } // get the booking ID for the transaction let force_bid = widget_instance.attr('data-pagebid'); // the widget method to call let call_method = 'doOffSessionCapture'; // make a request to load the bookings calendar VBOCore.doAjax( "<?php echo $this->getExecWidgetAjaxUri(); ?>", { widget_id: "<?php echo $this->getIdentifier(); ?>", call: call_method, return: 1, bid: force_bid, currency: widget_instance.find('input[data-vt-cc-field="currency"]').val(), amount: widget_instance.find('input[data-vt-cc-field="amount"]').val(), wrapper: wrapper, tmpl: "component" }, (response) => { try { let obj_res = typeof response === 'string' ? JSON.parse(response) : response; if (!obj_res.hasOwnProperty(call_method)) { console.error('Unexpected JSON response', obj_res); return false; } // turn flag on vbo_widget_vt_last_tn = 1; // update button status charge_cmd.removeClass('vbo-config-btn').addClass('btn-success').html(vbo_vt_icon_success + ' ' + Joomla.JText._('VBOUPLOADFILEDONE')); // check if we need to dismiss the modal widget let js_modal_id = widget_instance.attr('data-modalid'); if (js_modal_id) { setTimeout(() => { // dismiss modal widget VBOCore.emitEvent('vbo-dismiss-widget-modal' + js_modal_id); }, 1500); } } catch(err) { console.error('could not parse JSON response', err, response); } }, (error) => { // display error message alert(error.responseText); // restore button charge_cmd.prop('disabled', false); charge_cmd.find('i').replaceWith(vbo_vt_icon_error); } ); } /** * Generate the HTML skeleton loading string to build the form. */ function vboWidgetVTerminalFormSkeleton(wrapper) { var widget_instance = jQuery('#' + wrapper); if (!widget_instance.length) { return false; } var def_loading = ''; def_loading += '<div class="vbo-dashboard-guest-activity vbo-dashboard-guest-activity-skeleton">'; def_loading += ' <div class="vbo-dashboard-guest-activity-avatar">'; def_loading += ' <div class="vbo-skeleton-loading vbo-skeleton-loading-avatar"></div>'; def_loading += ' </div>'; def_loading += ' <div class="vbo-dashboard-guest-activity-content">'; def_loading += ' <div class="vbo-dashboard-guest-activity-content-head">'; def_loading += ' <div class="vbo-skeleton-loading vbo-skeleton-loading-title"></div>'; def_loading += ' </div>'; def_loading += ' <div class="vbo-dashboard-guest-activity-content-subhead">'; def_loading += ' <div class="vbo-skeleton-loading vbo-skeleton-loading-subtitle"></div>'; def_loading += ' </div>'; def_loading += ' <div class="vbo-dashboard-guest-activity-content-info-msg">'; def_loading += ' <div class="vbo-skeleton-loading vbo-skeleton-loading-content"></div>'; def_loading += ' </div>'; def_loading += ' </div>'; def_loading += '</div>'; widget_instance.find('.vbo-widget-vterminal-form').html(def_loading); } /** * Triggers when the multitask panel opens. */ function vboWidgetVTerminalMultitaskOpen(wrapper) { var widget_instance = jQuery('#' + wrapper); if (!widget_instance.length) { return false; } // check if a booking ID was set for this page var page_bid = widget_instance.attr('data-pagebid'); if (!page_bid || page_bid < 1) { return false; } // show loading skeletons vboWidgetVTerminalFormSkeleton(wrapper); // load data by injecting the current booking ID vboWidgetVTerminalFormLoad(wrapper, page_bid); } /** * Credit Card Detector Class. */ function VBOCreditCard(card) { this.set(card); } VBOCreditCard.prototype.set = function(card) { this.cards_logo_uri_base = '<?php echo VBO_ADMIN_URI . 'resources/js_upload/images/'; ?>'; this.card = []; for (var i = 0; i < card.length; i++) { var ch = card.charCodeAt(i); if (ch >= 48 && ch <= 57) { this.card.push(ch-48); } } } VBOCreditCard.prototype.get = function() { return this.card; } VBOCreditCard.prototype.isEnoughSpace = function(len) { return ( this.card.length >= len ); } VBOCreditCard.prototype.isEmpty = function() { return !this.isEnoughSpace(1); } VBOCreditCard.prototype.isValid = function() { const type = this.getCardType(); if (type.length && this.card.length == VBOCreditCard.properties[type].size) { return true; } return false; } VBOCreditCard.prototype.getNumberToIndex = function(i) { var n = 0; var factor = 1; for (i = i-1 ; i >= 0; i--) { n += factor * this.card[i]; factor *= 10; } return n; } VBOCreditCard.prototype.isVisa = function() { return this.matchBrandRanges([ [4] ]); } VBOCreditCard.prototype.isMasterCard = function() { return this.matchBrandRanges([ [51, 55], [2221, 2720] ]); } VBOCreditCard.prototype.isAmericanExpress = function() { return this.matchBrandRanges([ [34], [37] ]); } VBOCreditCard.prototype.isDiners = function() { return this.matchBrandRanges([ [300, 305], [36], [38, 39] ]); } VBOCreditCard.prototype.isDiscover = function() { return this.matchBrandRanges([ [6011], [65], [622126, 622925], [644, 649] ]); } VBOCreditCard.prototype.isJCB = function() { return this.matchBrandRanges([ [3528, 3589] ]); } VBOCreditCard.prototype.getCardType = function() { if (this.isVisa()) { return VBOCreditCard.VISA; } else if (this.isMasterCard()) { return VBOCreditCard.MASTERCARD; } else if (this.isAmericanExpress()) { return VBOCreditCard.AMERICAN_EXPRESS; } else if (this.isDiners()) { return VBOCreditCard.DINERS; } else if (this.isDiscover()) { return VBOCreditCard.DISCOVER; } else if (this.isJCB()) { return VBOCreditCard.JCB; } return ''; } VBOCreditCard.prototype.getCardLogoURI = function(type) { if (!type) { return ''; } return this.cards_logo_uri_base + type + '.png'; } VBOCreditCard.prototype.matchBrandRanges = function(ranges) { for (var i = 0; i < ranges.length; i++) { var r = ranges[i]; if (r.length == 1) { if (this.isEnoughSpace((''+r[0]).length) && this.getNumberToIndex((''+r[0]).length) == r[0]) { return true; } } else if (r.length == 2) { var len = Math.max( (''+r[0]).length, (''+r[1]).length ); if (this.isEnoughSpace(len)) { var val = this.getNumberToIndex(len); if (r[0] <= val && val <= r[1]) { return true; } } } } return false; } VBOCreditCard.prototype.formatCreditCard = function(card_type) { if (card_type === undefined) { card_type = this.getCardType(); } var blank_spaces = []; if (card_type.length > 0) { blank_spaces = VBOCreditCard.properties[card_type]['blank']; } var cc_str = ''; for (var i = 0; i < this.card.length; i++) { cc_str += this.card[i]; if (blank_spaces.indexOf(i+1) != -1) { cc_str += ' '; } } return cc_str; } VBOCreditCard.properties = { 'visa': { 'size': 16, 'blank': [4, 8, 12] }, 'mastercard': { 'size': 16, 'blank': [4, 8, 12] }, 'amex': { 'size': 15, 'blank': [4, 10] }, 'discover': { 'size': 16, 'blank': [4, 8, 12] }, 'diners': { 'size': 14, 'blank': [4, 8, 12] }, 'jcb': { 'size': 16, 'blank': [4, 8, 12] }, }; VBOCreditCard.VISA = 'visa'; VBOCreditCard.MASTERCARD = 'mastercard'; VBOCreditCard.AMERICAN_EXPRESS = 'amex'; VBOCreditCard.DINERS = 'diners'; VBOCreditCard.DISCOVER = 'discover'; VBOCreditCard.JCB = 'jcb'; </script> <?php } ?> <script type="text/javascript"> // store the last processed transaction var vbo_widget_vt_last_tn = null; jQuery(function() { // show loading skeletons vboWidgetVTerminalFormSkeleton('<?php echo $wrapper_id; ?>'); // when document is ready, load terminal form for this widget's instance vboWidgetVTerminalFormLoad('<?php echo $wrapper_id; ?>', '<?php echo $page_bid; ?>'); // subscribe to the multitask-panel-open event document.addEventListener(VBOCore.multitask_open_event, function() { vboWidgetVTerminalMultitaskOpen('<?php echo $wrapper_id; ?>'); }); // subscribe to the multitask-panel-close event to propagate the event for new transaction document.addEventListener(VBOCore.multitask_close_event, function() { if (vbo_widget_vt_last_tn) { // emit the event with data for anyone who is listening to it VBOCore.emitEvent('vbo_new_payment_transaction', { tn: vbo_widget_vt_last_tn }); } }); <?php if ($js_modal_id) { // widget can be dismissed through the modal ?> // subscribe to the modal-dismissed event to emit the event for the lastly created booking ID document.addEventListener(VBOCore.widget_modal_dismissed + '<?php echo $js_modal_id; ?>', function() { if (vbo_widget_vt_last_tn) { // emit the event with data for anyone who is listening to it VBOCore.emitEvent('vbo_new_payment_transaction', { tn: vbo_widget_vt_last_tn }); } }); <?php } ?> }); </script> <?php } /** * Attempts to invoke the eligible payment processor assigned to the given reservation. * * @param array $booking the reservation record as an associative array. * @param array $card the card details collected through the Virtual Terminal. * * @return ?object the payment processor dispatcher instance or null. */ protected function getPaymentProcessor(array $booking, array $card = []) { try { $processor = VBOModelReservation::getInstance($booking, true)->getPaymentProcessor($card); } catch (Exception $e) { $processor = null; } return $processor; } }