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 +372 -224 1.6.4 → 1.8.16 View file →
@@ -69,10 +69,12 @@
69 69 * In this case we return an array because this method requires "return":1.
70 70 */
71 71 public function loadTerminalForm()
72 72 {
73 + $dbo = JFactory::getDbo();
74 +
73 75 $wrapper = VikRequest::getString('wrapper', '', 'request');
74 - $bid = VikRequest::getInt('bid', 0, 'request');
76 + $bid = VikRequest::getString('bid', '', 'request');
75 77
76 78 if (!$bid) {
77 79 // booking ID is mandatory
78 80 VBOHttpDocument::getInstance()->close(500, JText::translate('VBPEDITBUSYONE'));
@@ -81,14 +83,36 @@
81 83 // get booking details
82 84 $booking_info = VikBooking::getBookingInfoFromID($bid);
83 85
84 86 if (!$booking_info) {
85 - // booking record not found
86 - VBOHttpDocument::getInstance()->close(404, JText::translate('VBPEDITBUSYONE'));
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 + }
87 111 }
88 112
89 - // check if direct charge is supported by the current payment processor
90 - if (!$this->getPaymentProcessor($booking_info)) {
113 + // ensure a valid payment processor is assigned to the reservation
114 + if (!($processor = $this->getPaymentProcessor($booking_info))) {
91 115 // unable to proceed due to unsupported transaction
92 116 return [
93 117 'html' => '<p class="err">' . JText::translate('VBO_PAY_PROCESS_NO_DIRECT_CHARGE') . '</p>',
94 118 ];
@@ -93,58 +117,22 @@
93 117 'html' => '<p class="err">' . JText::translate('VBO_PAY_PROCESS_NO_DIRECT_CHARGE') . '</p>',
94 118 ];
95 119 }
96 120
97 - // build complete credit card payload, if any
98 - $cc_payload_str = '';
99 - $booking_info['paymentlog'] = (string)$booking_info['paymentlog'];
100 - if (stripos($booking_info['paymentlog'], 'card number') !== false && strpos($booking_info['paymentlog'], '*') !== false) {
101 - // matched a log for an OTA CC
102 - $cc_payload_str = $booking_info['paymentlog'];
103 - } elseif (preg_match("/(([\d\*]{4,4}\s*){4,4})|(([\d\*]{4,6}\s*){3,3})/", $booking_info['paymentlog'])) {
104 - // matched a credit card
105 - $cc_payload_str = $booking_info['paymentlog'];
106 - }
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() : [];
107 124
108 - // check if this is an OTA reservation with remotely decoded CC details
109 - $remote_cc_data = [];
110 - if ($this->vcm_exists && !empty($booking_info['idorderota']) && !empty($booking_info['channel'])) {
111 - // make sure the permissions are met
112 - if (!JFactory::getUser()->authorise('core.admin', 'com_vikchannelmanager')) {
113 - // insufficient permissions to handle CC details
114 - return [
115 - 'html' => '<p class="err">' . JText::translate('JERROR_ALERTNOAUTHOR') . '</p>',
116 - ];
117 - }
118 -
119 - // channel source
120 - $channel_source = (string)$booking_info['channel'];
121 - if (strpos($booking_info['channel'], '_') !== false) {
122 - $channelparts = explode('_', $booking_info['channel']);
123 - $channel_source = $channelparts[0];
124 - }
125 -
126 - // only updated versions of VCM will support remote CC decoding for OTA reservations
127 - if (class_exists('VCMOtaBooking')) {
128 - // invoke the OTA Booking helper class from VCM
129 - $cc_helper = VCMOtaBooking::getInstance([
130 - 'channel_source' => $channel_source,
131 - 'ota_id' => $booking_info['idorderota'],
132 - ], $anew = true);
133 -
134 - if (method_exists($cc_helper, 'decodeCreditCardDetails')) {
135 - $remote_cc_data = $cc_helper->decodeCreditCardDetails();
136 - // make sure the response was valid
137 - if (!$remote_cc_data || !empty($remote_cc_data['error'])) {
138 - // we ignore the error by simply resetting the array
139 - $remote_cc_data = [];
140 - }
141 - }
142 - }
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 + ];
143 131 }
144 132
145 - // merge remotely decoded CC details to parsed payment log (if any)
146 - $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();
147 135
148 136 // currency code
149 137 $currency_code = !empty($booking_info['chcurrency']) ? $booking_info['chcurrency'] : VikBooking::getCurrencyName();
150 138 if (empty($currency_code) || strlen((string)$currency_code) != 3) {
@@ -172,8 +160,28 @@
172 160 $hidden_tn_vals[$key] = $value;
173 161 }
174 162 }
175 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 +
176 184 // start output buffering
177 185 ob_start();
178 186
179 187 ?>
@@ -189,53 +197,85 @@
189 197 </div>
190 198 <div class="vbo-vterminal-cc-row vbo-vterminal-cc-row-amount">
191 199 <div class="vbo-vterminal-cc-lbl"><?php echo JText::translate('VBO_CC_AMOUNT'); ?></div>
192 200 <div class="vbo-vterminal-cc-val">
193 - <input type="number" value="<?php echo $booking_info['total']; ?>" min="0" step="any" data-vt-cc-field="amount" />
201 + <input type="number" value="<?php echo $default_total; ?>" min="0" step="any" data-vt-cc-field="amount" />
194 202 </div>
195 203 </div>
196 204 </div>
197 205
198 - <div class="vbo-vterminal-cc-row-group vbo-vterminal-cc-row-group-cardholder">
199 - <div class="vbo-vterminal-cc-row vbo-vterminal-cc-row-cardholder">
200 - <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">
201 212 <div class="vbo-vterminal-cc-val">
202 - <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>
203 214 </div>
204 215 </div>
205 216 </div>
217 + <?php
218 + }
219 + ?>
206 220
207 - <div class="vbo-vterminal-cc-row-group vbo-vterminal-cc-row-group-ccpan">
208 - <div class="vbo-vterminal-cc-row vbo-vterminal-cc-row-ccpan">
209 - <div class="vbo-vterminal-cc-lbl"><?php echo JText::translate('VBO_CC_NUMBER'); ?></div>
210 - <div class="vbo-vterminal-cc-val vbo-vterminal-cc-val-withlogo">
211 - <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" />
212 - <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>
213 230 </div>
214 231 </div>
215 - </div>
232 + <?php
233 + }
234 + ?>
216 235
217 - <div class="vbo-vterminal-cc-row-group vbo-vterminal-cc-row-group-ccextra">
218 - <div class="vbo-vterminal-cc-row vbo-vterminal-cc-row-ccexpiry">
219 - <div class="vbo-vterminal-cc-lbl"><?php echo JText::translate('VBO_CC_EXPIRY_DT'); ?></div>
220 - <div class="vbo-vterminal-cc-val">
221 - <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>
222 242 </div>
223 243 </div>
224 - <div class="vbo-vterminal-cc-row vbo-vterminal-cc-row-cvc">
225 - <div class="vbo-vterminal-cc-lbl"><?php echo JText::translate('VBO_CC_CVV'); ?></div>
226 - <div class="vbo-vterminal-cc-val">
227 - <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>
228 252 </div>
229 253 </div>
230 - </div>
231 254
232 - <div class="vbo-vterminal-cc-row-group vbo-vterminal-cc-row-group-submit">
233 - <div class="vbo-vterminal-cc-row vbo-vterminal-cc-row-submit">
234 - <div class="vbo-vterminal-cc-val">
235 - <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>
236 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>
237 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 +
238 278 </div>
239 279
240 280 <?php
241 281 // print hidden transaction values, if any
@@ -452,9 +492,9 @@
452 492 // update booking details
453 493 $dbo = JFactory::getDbo();
454 494
455 495 // get the amount paid
456 - $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;
457 497
458 498 // get the log string, if any
459 499 $tn_log = !empty($array_result['log']) ? $array_result['log'] : '';
460 500
@@ -498,8 +538,24 @@
498 538 ];
499 539 }
500 540 }
501 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 +
502 558 // current admin-user
503 559 $now_user = JFactory::getUser();
504 560
505 561 // Booking History
@@ -512,8 +568,157 @@
512 568 ];
513 569 }
514 570
515 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 + /**
516 721 * Preload the necessary assets.
517 722 *
518 723 * @return void
519 724 */
@@ -526,13 +731,13 @@
526 731 /**
527 732 * Main method to invoke the widget. Contents will be loaded
528 733 * through AJAX requests, not via PHP when the page loads.
529 734 *
530 - * @param VBOMultitaskData $data
735 + * @param ?VBOMultitaskData $data
531 736 *
532 737 * @return void
533 738 */
534 - public function render(VBOMultitaskData $data = null)
739 + public function render(?VBOMultitaskData $data = null)
535 740 {
536 741 // increase widget's instance counter
537 742 static::$instance_counter++;
538 743
@@ -554,9 +759,9 @@
554 759 $page_bid = 0;
555 760 $js_modal_id = '';
556 761 if ($data) {
557 762 $is_modal_rendering = $data->isModalRendering();
558 - $page_bid = $data->getBookingID();
763 + $page_bid = $data->getBookingID() ?: $this->options()->fetchBookingId();
559 764 if ($is_modal_rendering) {
560 765 // get modal JS identifier
561 766 $js_modal_id = $data->getModalJsIdentifier();
562 767 }
@@ -612,9 +817,9 @@
612 817 }
613 818
614 819 // check if multitask data passed a booking ID for the current page
615 820 var force_bid = 0;
616 - if (typeof page_bid !== 'undefined' && page_bid && !isNaN(page_bid)) {
821 + if (typeof page_bid !== 'undefined' && page_bid) {
617 822 force_bid = page_bid;
618 823 }
619 824
620 825 // the widget method to call
@@ -663,9 +868,9 @@
663 868 return false;
664 869 }
665 870
666 871 // the trigger button
667 - 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');
668 873
669 874 // disable button
670 875 charge_cmd.prop('disabled', true);
671 876
@@ -735,8 +940,83 @@
735 940 );
736 941 }
737 942
738 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 + /**
739 1019 * Generate the HTML skeleton loading string to build the form.
740 1020 */
741 1021 function vboWidgetVTerminalFormSkeleton(wrapper) {
742 1022 var widget_instance = jQuery('#' + wrapper);
@@ -1007,9 +1287,9 @@
1007 1287
1008 1288 // show loading skeletons
1009 1289 vboWidgetVTerminalFormSkeleton('<?php echo $wrapper_id; ?>');
1010 1290
1011 - // 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
1012 1292 vboWidgetVTerminalFormLoad('<?php echo $wrapper_id; ?>', '<?php echo $page_bid; ?>');
1013 1293
1014 1294 // subscribe to the multitask-panel-open event
1015 1295 document.addEventListener(VBOCore.multitask_open_event, function() {
@@ -1015,9 +1295,9 @@
1015 1295 document.addEventListener(VBOCore.multitask_open_event, function() {
1016 1296 vboWidgetVTerminalMultitaskOpen('<?php echo $wrapper_id; ?>');
1017 1297 });
1018 1298
1019 - // 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
1020 1300 document.addEventListener(VBOCore.multitask_close_event, function() {
1021 1301 if (vbo_widget_vt_last_tn) {
1022 1302 // emit the event with data for anyone who is listening to it
1023 1303 VBOCore.emitEvent('vbo_new_payment_transaction', {
@@ -1055,149 +1335,17 @@
1055 1335 *
1056 1336 * @param array $booking the reservation record as an associative array.
1057 1337 * @param array $card the card details collected through the Virtual Terminal.
1058 1338 *
1059 - * @return object|null the payment processor dispatcher instance or null.
1339 + * @return ?object the payment processor dispatcher instance or null.
1060 1340 */
1061 1341 protected function getPaymentProcessor(array $booking, array $card = [])
1062 1342 {
1063 - $processor = null;
1064 - $payment = [];
1065 -
1066 - if (!empty($booking['idpayment'])) {
1067 - $payment = VikBooking::getPayment($booking['idpayment']);
1343 + try {
1344 + $processor = VBOModelReservation::getInstance($booking, true)->getPaymentProcessor($card);
1345 + } catch (Exception $e) {
1346 + $processor = null;
1068 1347 }
1069 1348
1070 - if ($card) {
1071 - // inject CC details for the payment processor
1072 - $booking['card'] = $card;
1073 - }
1074 -
1075 - if ($payment && VBOPlatformDetection::isWordPress()) {
1076 - /**
1077 - * @wponly The payment gateway is loaded
1078 - * through the apposite dispatcher.
1079 - */
1080 - JLoader::import('adapter.payment.dispatcher');
1081 - $processor = JPaymentDispatcher::getInstance('vikbooking', $payment['file'], $booking, $payment['params']);
1082 - } elseif ($payment && VBOPlatformDetection::isJoomla()) {
1083 - /**
1084 - * @joomlaonly The Payment Factory library will invoke the gateway.
1085 - */
1086 - require_once VBO_ADMIN_PATH . DIRECTORY_SEPARATOR . 'payments' . DIRECTORY_SEPARATOR . 'libraries' . DIRECTORY_SEPARATOR . 'factory.php';
1087 - $processor = VBOPaymentFactory::getPaymentInstance($payment['file'], $booking, $payment['params']);
1088 - }
1089 -
1090 - if ($processor && method_exists($processor, 'isDirectChargeSupported') && $processor->isDirectChargeSupported()) {
1091 - // set payment method flag loaded
1092 - $this->payment_method = $payment;
1093 -
1094 - // return the valid payment processor instance
1095 - return $processor;
1096 - }
1097 -
1098 - return null;
1099 - }
1100 -
1101 - /**
1102 - * Given a raw string of credit card key-value pairs from payments log,
1103 - * parse the corresponding keys and values into an associative array.
1104 - * In case of conflicting keys with the remotely decoded CC details,
1105 - * attempts to replace the masked numbers with asterisks.
1106 - *
1107 - * @param string $cc_payload the raw CC details from payment logs.
1108 - * @param array $remote_cc_data associative array of decoded CC data.
1109 - *
1110 - * @return array associative or empty array.
1111 - */
1112 - protected function parseCreditCardValuePairs($cc_payload, array $remote_cc_data = [])
1113 - {
1114 - $cc_value_pairs = [];
1115 -
1116 - if (empty($cc_payload)) {
1117 - return $cc_value_pairs;
1118 - }
1119 -
1120 - $cc_lines = preg_split("/(\r\n|\n|\r)/", $cc_payload);
1121 -
1122 - foreach ($cc_lines as $cc_line) {
1123 - if (strpos($cc_line, ':') === false) {
1124 - continue;
1125 - }
1126 -
1127 - $cc_line_parts = explode(':', $cc_line);
1128 -
1129 - if (empty($cc_line_parts[0]) || !strlen(trim($cc_line_parts[1]))) {
1130 - continue;
1131 - }
1132 -
1133 - $key = str_replace(' ', '_', strtolower($cc_line_parts[0]));
1134 - $value = trim($cc_line_parts[1]);
1135 -
1136 - if (!empty($remote_cc_data[$key]) && is_string($remote_cc_data[$key]) && strpos($value, '*') !== false) {
1137 - // replace masked numbers with remote content
1138 - $value = $this->replaceMaskedNumbers($value, $remote_cc_data[$key]);
1139 - }
1140 -
1141 - $cc_value_pairs[$key] = $value;
1142 - }
1143 -
1144 - return $cc_value_pairs;
1145 - }
1146 -
1147 - /**
1148 - * Given a local and a remote credit card number string with
1149 - * masked symbols, replaces the values in the corresponding
1150 - * positions with the unmasked numbers.
1151 - *
1152 - * @param string $local current string with masked values.
1153 - * @param string $remote remote string with unmasked values.
1154 - *
1155 - * @return string the local string with unmasked values.
1156 - */
1157 - protected function replaceMaskedNumbers($local, $remote)
1158 - {
1159 - // split anything but numbers
1160 - $numbers = preg_split("/([^0-9]+)/", trim($remote));
1161 -
1162 - if ($numbers) {
1163 - // filter empty values
1164 - $numbers = array_filter($numbers);
1165 - }
1166 -
1167 - if (!$numbers) {
1168 - // unable to proceed
1169 - return $local;
1170 - }
1171 -
1172 - // split anything but stars (asterisks)
1173 - $stars = preg_split("/([^\*]+)/", trim($local));
1174 -
1175 - if ($stars) {
1176 - // filter empty values
1177 - $stars = array_filter($stars);
1178 - }
1179 -
1180 - if (!$stars) {
1181 - // unable to proceed
1182 - return $local;
1183 - }
1184 -
1185 - // replace masked symbols with numbers at their first occurrence
1186 - foreach ($numbers as $k => $unmasked) {
1187 - if (!isset($stars[$k])) {
1188 - continue;
1189 - }
1190 -
1191 - $masked_pos = strpos($local, $stars[$k]);
1192 -
1193 - if ($masked_pos === false) {
1194 - continue;
1195 - }
1196 -
1197 - $local = substr_replace($local, $unmasked, $masked_pos, strlen($stars[$k]));
1198 - }
1199 -
1200 - // return the string with possibly unmasked values
1201 - return $local;
1349 + return $processor;
1202 1350 }
1203 1351 }