PluginProbe
VikBooking Hotel Booking Engine & PMS / 1.8.16
VikBooking Hotel Booking Engine & PMS v1.8.16
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
← All changes | admin/helpers/widgets/virtual_terminal.php +331 -230 1.6.8 → 1.8.16 View file →
@@ -109,10 +109,10 @@
109 109 VBOHttpDocument::getInstance()->close(404, JText::translate('VBPEDITBUSYONE'));
110 110 }
111 111 }
112 112
113 - // check if direct charge is supported by the current payment processor
114 - if (!$this->getPaymentProcessor($booking_info)) {
113 + // ensure a valid payment processor is assigned to the reservation
114 + if (!($processor = $this->getPaymentProcessor($booking_info))) {
115 115 // unable to proceed due to unsupported transaction
116 116 return [
117 117 'html' => '<p class="err">' . JText::translate('VBO_PAY_PROCESS_NO_DIRECT_CHARGE') . '</p>',
118 118 ];
@@ -117,58 +117,22 @@
117 117 'html' => '<p class="err">' . JText::translate('VBO_PAY_PROCESS_NO_DIRECT_CHARGE') . '</p>',
118 118 ];
119 119 }
120 120
121 - // build complete credit card payload, if any
122 - $cc_payload_str = '';
123 - $booking_info['paymentlog'] = (string)$booking_info['paymentlog'];
124 - if (stripos($booking_info['paymentlog'], 'card number') !== false && strpos($booking_info['paymentlog'], '*') !== false) {
125 - // matched a log for an OTA CC
126 - $cc_payload_str = $booking_info['paymentlog'];
127 - } elseif (preg_match("/(([\d\*]{4,4}\s*){4,4})|(([\d\*]{4,6}\s*){3,3})/", $booking_info['paymentlog'])) {
128 - // matched a credit card
129 - $cc_payload_str = $booking_info['paymentlog'];
130 - }
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() : [];
131 124
132 - // check if this is an OTA reservation with remotely decoded CC details
133 - $remote_cc_data = [];
134 - if ($this->vcm_exists && !empty($booking_info['idorderota']) && !empty($booking_info['channel'])) {
135 - // make sure the permissions are met
136 - if (!JFactory::getUser()->authorise('core.admin', 'com_vikchannelmanager')) {
137 - // insufficient permissions to handle CC details
138 - return [
139 - 'html' => '<p class="err">' . JText::translate('JERROR_ALERTNOAUTHOR') . '</p>',
140 - ];
141 - }
142 -
143 - // channel source
144 - $channel_source = (string)$booking_info['channel'];
145 - if (strpos($booking_info['channel'], '_') !== false) {
146 - $channelparts = explode('_', $booking_info['channel']);
147 - $channel_source = $channelparts[0];
148 - }
149 -
150 - // only updated versions of VCM will support remote CC decoding for OTA reservations
151 - if (class_exists('VCMOtaBooking')) {
152 - // invoke the OTA Booking helper class from VCM
153 - $cc_helper = VCMOtaBooking::getInstance([
154 - 'channel_source' => $channel_source,
155 - 'ota_id' => $booking_info['idorderota'],
156 - ], $anew = true);
157 -
158 - if (method_exists($cc_helper, 'decodeCreditCardDetails')) {
159 - $remote_cc_data = $cc_helper->decodeCreditCardDetails();
160 - // make sure the response was valid
161 - if (!$remote_cc_data || !empty($remote_cc_data['error'])) {
162 - // we ignore the error by simply resetting the array
163 - $remote_cc_data = [];
164 - }
165 - }
166 - }
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 + ];
167 131 }
168 132
169 - // merge remotely decoded CC details to parsed payment log (if any)
170 - $cc_value_pairs = array_merge($remote_cc_data, $this->parseCreditCardValuePairs($cc_payload_str, $remote_cc_data));
133 + // get the reservation credit card value pairs, if any
134 + $cc_value_pairs = VBOModelReservation::getInstance($booking_info, true)->getCardValuePairs();
171 135
172 136 // currency code
173 137 $currency_code = !empty($booking_info['chcurrency']) ? $booking_info['chcurrency'] : VikBooking::getCurrencyName();
174 138 if (empty($currency_code) || strlen((string)$currency_code) != 3) {
@@ -200,8 +164,9 @@
200 164 /**
201 165 * Consider calculating an outstanding balance in case of previous payments.
202 166 *
203 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.
204 169 */
205 170 $default_total = $booking_info['total'];
206 171 if ($booking_info['totpaid'] > 0 && $booking_info['totpaid'] < $booking_info['total']) {
207 172 // default to the outstanding balance
@@ -207,8 +172,16 @@
207 172 // default to the outstanding balance
208 173 $default_total = $booking_info['total'] - $booking_info['totpaid'];
209 174 }
210 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 +
211 184 // start output buffering
212 185 ob_start();
213 186
214 187 ?>
@@ -229,48 +202,80 @@
229 202 </div>
230 203 </div>
231 204 </div>
232 205
233 - <div class="vbo-vterminal-cc-row-group vbo-vterminal-cc-row-group-cardholder">
234 - <div class="vbo-vterminal-cc-row vbo-vterminal-cc-row-cardholder">
235 - <div class="vbo-vterminal-cc-lbl"><?php echo JText::translate('VBISNOMINATIVE'); ?></div>
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">
236 212 <div class="vbo-vterminal-cc-val">
237 - <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" />
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>
238 214 </div>
239 215 </div>
240 216 </div>
217 + <?php
218 + }
219 + ?>
241 220
242 - <div class="vbo-vterminal-cc-row-group vbo-vterminal-cc-row-group-ccpan">
243 - <div class="vbo-vterminal-cc-row vbo-vterminal-cc-row-ccpan">
244 - <div class="vbo-vterminal-cc-lbl"><?php echo JText::translate('VBO_CC_NUMBER'); ?></div>
245 - <div class="vbo-vterminal-cc-val vbo-vterminal-cc-val-withlogo">
246 - <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" />
247 - <span class="vbo-vterminal-cc-type-logo"></span>
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>
248 230 </div>
249 231 </div>
250 - </div>
232 + <?php
233 + }
234 + ?>
251 235
252 - <div class="vbo-vterminal-cc-row-group vbo-vterminal-cc-row-group-ccextra">
253 - <div class="vbo-vterminal-cc-row vbo-vterminal-cc-row-ccexpiry">
254 - <div class="vbo-vterminal-cc-lbl"><?php echo JText::translate('VBO_CC_EXPIRY_DT'); ?></div>
255 - <div class="vbo-vterminal-cc-val">
256 - <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" />
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>
257 242 </div>
258 243 </div>
259 - <div class="vbo-vterminal-cc-row vbo-vterminal-cc-row-cvc">
260 - <div class="vbo-vterminal-cc-lbl"><?php echo JText::translate('VBO_CC_CVV'); ?></div>
261 - <div class="vbo-vterminal-cc-val">
262 - <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" />
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>
263 252 </div>
264 253 </div>
265 - </div>
266 254
267 - <div class="vbo-vterminal-cc-row-group vbo-vterminal-cc-row-group-submit">
268 - <div class="vbo-vterminal-cc-row vbo-vterminal-cc-row-submit">
269 - <div class="vbo-vterminal-cc-val">
270 - <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>
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>
271 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>
272 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 +
273 278 </div>
274 279
275 280 <?php
276 281 // print hidden transaction values, if any
@@ -487,9 +492,9 @@
487 492 // update booking details
488 493 $dbo = JFactory::getDbo();
489 494
490 495 // get the amount paid
491 - $tn_amount = isset($array_result['tot_paid']) ? (float)$array_result['tot_paid'] : null;
496 + $tn_amount = isset($array_result['tot_paid']) ? (float) $array_result['tot_paid'] : null;
492 497
493 498 // get the log string, if any
494 499 $tn_log = !empty($array_result['log']) ? $array_result['log'] : '';
495 500
@@ -533,8 +538,24 @@
533 538 ];
534 539 }
535 540 }
536 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 +
537 558 // current admin-user
538 559 $now_user = JFactory::getUser();
539 560
540 561 // Booking History
@@ -547,8 +568,157 @@
547 568 ];
548 569 }
549 570
550 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 + /**
551 721 * Preload the necessary assets.
552 722 *
553 723 * @return void
554 724 */
@@ -561,13 +731,13 @@
561 731 /**
562 732 * Main method to invoke the widget. Contents will be loaded
563 733 * through AJAX requests, not via PHP when the page loads.
564 734 *
565 - * @param VBOMultitaskData $data
735 + * @param ?VBOMultitaskData $data
566 736 *
567 737 * @return void
568 738 */
569 - public function render(VBOMultitaskData $data = null)
739 + public function render(?VBOMultitaskData $data = null)
570 740 {
571 741 // increase widget's instance counter
572 742 static::$instance_counter++;
573 743
@@ -698,9 +868,9 @@
698 868 return false;
699 869 }
700 870
701 871 // the trigger button
702 - var charge_cmd = widget_instance.find('.vbo-vterminal-cc-row-submit').find('button');
872 + var charge_cmd = widget_instance.find('.vbo-vterminal-cc-row-submit[data-tn-type="direct-charge"]').find('button');
703 873
704 874 // disable button
705 875 charge_cmd.prop('disabled', true);
706 876
@@ -770,8 +940,83 @@
770 940 );
771 941 }
772 942
773 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 + /**
774 1019 * Generate the HTML skeleton loading string to build the form.
775 1020 */
776 1021 function vboWidgetVTerminalFormSkeleton(wrapper) {
777 1022 var widget_instance = jQuery('#' + wrapper);
@@ -1042,9 +1287,9 @@
1042 1287
1043 1288 // show loading skeletons
1044 1289 vboWidgetVTerminalFormSkeleton('<?php echo $wrapper_id; ?>');
1045 1290
1046 - // when document is ready, load bookings calendar for this widget's instance
1291 + // when document is ready, load terminal form for this widget's instance
1047 1292 vboWidgetVTerminalFormLoad('<?php echo $wrapper_id; ?>', '<?php echo $page_bid; ?>');
1048 1293
1049 1294 // subscribe to the multitask-panel-open event
1050 1295 document.addEventListener(VBOCore.multitask_open_event, function() {
@@ -1050,9 +1295,9 @@
1050 1295 document.addEventListener(VBOCore.multitask_open_event, function() {
1051 1296 vboWidgetVTerminalMultitaskOpen('<?php echo $wrapper_id; ?>');
1052 1297 });
1053 1298
1054 - // subscribe to the multitask-panel-close event to emit the event for the lastly created booking ID
1299 + // subscribe to the multitask-panel-close event to propagate the event for new transaction
1055 1300 document.addEventListener(VBOCore.multitask_close_event, function() {
1056 1301 if (vbo_widget_vt_last_tn) {
1057 1302 // emit the event with data for anyone who is listening to it
1058 1303 VBOCore.emitEvent('vbo_new_payment_transaction', {
@@ -1090,161 +1335,17 @@
1090 1335 *
1091 1336 * @param array $booking the reservation record as an associative array.
1092 1337 * @param array $card the card details collected through the Virtual Terminal.
1093 1338 *
1094 - * @return object|null the payment processor dispatcher instance or null.
1339 + * @return ?object the payment processor dispatcher instance or null.
1095 1340 */
1096 1341 protected function getPaymentProcessor(array $booking, array $card = [])
1097 1342 {
1098 - $processor = null;
1099 - $payment = [];
1100 -
1101 - if (!empty($booking['idpayment'])) {
1102 - $payment = VikBooking::getPayment($booking['idpayment']);
1343 + try {
1344 + $processor = VBOModelReservation::getInstance($booking, true)->getPaymentProcessor($card);
1345 + } catch (Exception $e) {
1346 + $processor = null;
1103 1347 }
1104 1348
1105 - if ($card) {
1106 - // inject CC details for the payment processor
1107 - $booking['card'] = $card;
1108 - }
1109 -
1110 - if ($payment && VBOPlatformDetection::isWordPress()) {
1111 - /**
1112 - * @wponly The payment gateway is loaded
1113 - * through the apposite dispatcher.
1114 - */
1115 - JLoader::import('adapter.payment.dispatcher');
1116 - $processor = JPaymentDispatcher::getInstance('vikbooking', $payment['file'], $booking, $payment['params']);
1117 - } elseif ($payment && VBOPlatformDetection::isJoomla()) {
1118 - /**
1119 - * @joomlaonly The Payment Factory library will invoke the gateway.
1120 - */
1121 - require_once VBO_ADMIN_PATH . DIRECTORY_SEPARATOR . 'payments' . DIRECTORY_SEPARATOR . 'libraries' . DIRECTORY_SEPARATOR . 'factory.php';
1122 - $processor = VBOPaymentFactory::getPaymentInstance($payment['file'], $booking, $payment['params']);
1123 - }
1124 -
1125 - if ($processor && method_exists($processor, 'isDirectChargeSupported') && $processor->isDirectChargeSupported()) {
1126 - // set payment method flag loaded
1127 - $this->payment_method = $payment;
1128 -
1129 - // return the valid payment processor instance
1130 - return $processor;
1131 - }
1132 -
1133 - return null;
1134 - }
1135 -
1136 - /**
1137 - * Given a raw string of credit card key-value pairs from payments log,
1138 - * parse the corresponding keys and values into an associative array.
1139 - * In case of conflicting keys with the remotely decoded CC details,
1140 - * attempts to replace the masked numbers with asterisks.
1141 - *
1142 - * @param string $cc_payload the raw CC details from payment logs.
1143 - * @param array $remote_cc_data associative array of decoded CC data.
1144 - *
1145 - * @return array associative or empty array.
1146 - */
1147 - protected function parseCreditCardValuePairs($cc_payload, array $remote_cc_data = [])
1148 - {
1149 - $cc_value_pairs = [];
1150 -
1151 - if (empty($cc_payload)) {
1152 - return $cc_value_pairs;
1153 - }
1154 -
1155 - $cc_lines = preg_split("/(\r\n|\n|\r)/", $cc_payload);
1156 -
1157 - foreach ($cc_lines as $cc_line) {
1158 - if (strpos($cc_line, ':') === false) {
1159 - continue;
1160 - }
1161 -
1162 - $cc_line_parts = explode(':', $cc_line);
1163 -
1164 - if (empty($cc_line_parts[0]) || !strlen(trim($cc_line_parts[1]))) {
1165 - continue;
1166 - }
1167 -
1168 - $key = str_replace(' ', '_', strtolower($cc_line_parts[0]));
1169 - $value = trim($cc_line_parts[1]);
1170 -
1171 - if (isset($cc_value_pairs[$key])) {
1172 - /**
1173 - * Do not overwrite existing keys because this probably means that the
1174 - * credit card was updated by an OTA like Booking.com, hence the payment
1175 - * logs string in VBO may contain the information of two different cards.
1176 - * New credit card details are always pre-pended by VCM in the payment logs.
1177 - *
1178 - * @since 1.16.5 (J) - 1.6.5 (WP)
1179 - */
1180 - continue;
1181 - }
1182 -
1183 - if (!empty($remote_cc_data[$key]) && is_string($remote_cc_data[$key]) && strpos($value, '*') !== false) {
1184 - // replace masked numbers with remote content
1185 - $value = $this->replaceMaskedNumbers($value, $remote_cc_data[$key]);
1186 - }
1187 -
1188 - $cc_value_pairs[$key] = $value;
1189 - }
1190 -
1191 - return $cc_value_pairs;
1192 - }
1193 -
1194 - /**
1195 - * Given a local and a remote credit card number string with
1196 - * masked symbols, replaces the values in the corresponding
1197 - * positions with the unmasked numbers.
1198 - *
1199 - * @param string $local current string with masked values.
1200 - * @param string $remote remote string with unmasked values.
1201 - *
1202 - * @return string the local string with unmasked values.
1203 - */
1204 - protected function replaceMaskedNumbers($local, $remote)
1205 - {
1206 - // split anything but numbers
1207 - $numbers = preg_split("/([^0-9]+)/", trim($remote));
1208 -
1209 - if ($numbers) {
1210 - // filter empty values
1211 - $numbers = array_filter($numbers);
1212 - }
1213 -
1214 - if (!$numbers) {
1215 - // unable to proceed
1216 - return $local;
1217 - }
1218 -
1219 - // split anything but stars (asterisks)
1220 - $stars = preg_split("/([^\*]+)/", trim($local));
1221 -
1222 - if ($stars) {
1223 - // filter empty values
1224 - $stars = array_filter($stars);
1225 - }
1226 -
1227 - if (!$stars) {
1228 - // unable to proceed
1229 - return $local;
1230 - }
1231 -
1232 - // replace masked symbols with numbers at their first occurrence
1233 - foreach ($numbers as $k => $unmasked) {
1234 - if (!isset($stars[$k])) {
1235 - continue;
1236 - }
1237 -
1238 - $masked_pos = strpos($local, $stars[$k]);
1239 -
1240 - if ($masked_pos === false) {
1241 - continue;
1242 - }
1243 -
1244 - $local = substr_replace($local, $unmasked, $masked_pos, strlen($stars[$k]));
1245 - }
1246 -
1247 - // return the string with possibly unmasked values
1248 - return $local;
1349 + return $processor;
1249 1350 }
1250 1351 }