PluginProbe
VikBooking Hotel Booking Engine & PMS / trunk
VikBooking Hotel Booking Engine & PMS vtrunk
1.8.15 1.8.14 1.8.13 1.8.12 1.8.11 1.8.10 1.8.9 1.8.6 1.8.7 1.8.8 trunk 1.6.0 1.6.1 1.6.2 1.6.3 1.6.4 1.6.5 1.6.6 1.6.7 1.6.8 1.6.9 1.7.0 1.7.1 1.7.2 1.7.3 All 36 releases
vikbooking / admin / helpers / src / model / payschedules.php

payschedules.php in VikBooking Hotel Booking Engine & PMS trunk, at admin/helpers/src/model/payschedules.php

453 lines 14.5 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * @package VikBooking
4 * @subpackage core
5 * @author E4J s.r.l.
6 * @copyright Copyright (C) 2024 E4J s.r.l. All Rights Reserved.
7 * @license http://www.gnu.org/licenses/gpl-2.0.html GNU/GPL
8 * @link https://vikwp.com
9 */
10
11 // No direct access
12 defined('ABSPATH') or die('No script kiddies please!');
13
14 /**
15 * VikBooking model Payment Schedules.
16 *
17 * @since 1.16.10 (J) - 1.6.10 (WP)
18 */
19 class VBOModelPayschedules
20 {
21 /** @var array */
22 protected $booking = [];
23
24 /**
25 * Proxy for immediately accessing the object.
26 *
27 * @return VBOModelPayschedules
28 */
29 public static function getInstance()
30 {
31 return new static;
32 }
33
34 /**
35 * Class constructor.
36 */
37 public function __construct()
38 {}
39
40 /**
41 * Sets the current booking record details.
42 *
43 * @param array $booking The booking record details.
44 *
45 * @return self
46 */
47 public function setBooking(array $booking)
48 {
49 $this->booking = $booking;
50
51 return $this;
52 }
53
54 /**
55 * Stores a new payment schedule record.
56 *
57 * @param array|object $record The record to store.
58 *
59 * @return int|null The new record ID or null.
60 */
61 public function save($record)
62 {
63 $dbo = JFactory::getDbo();
64
65 $record = (object) $record;
66
67 $dbo->insertObject('#__vikbooking_payschedules', $record, 'id');
68
69 return $record->id ?? null;
70 }
71
72 /**
73 * Updates an existing payment schedule record.
74 *
75 * @param array|object $record The record details to update.
76 *
77 * @return bool
78 */
79 public function update($record)
80 {
81 $dbo = JFactory::getDbo();
82
83 $record = (object) $record;
84
85 return (bool) $dbo->updateObject('#__vikbooking_payschedules', $record, 'id');
86 }
87
88 /**
89 * Item loading implementation.
90 *
91 * @param mixed $pk An optional primary key value to load the row by,
92 * or an associative array of fields to match.
93 *
94 * @return object|null The record object on success, null otherwise.
95 */
96 public function getItem($pk)
97 {
98 $dbo = JFactory::getDbo();
99
100 $q = $dbo->getQuery(true)
101 ->select('*')
102 ->from($dbo->qn('#__vikbooking_payschedules'));
103
104 if (is_array($pk)) {
105 foreach ($pk as $column => $value) {
106 $q->where($dbo->qn($column) . ' = ' . $dbo->q($value));
107 }
108 } else {
109 $q->where($dbo->qn('id') . ' = ' . (int) $pk);
110 }
111
112 $dbo->setQuery($q, 0, 1);
113
114 $record = $dbo->loadObject();
115
116 if ($record) {
117 $this->normalizeObject($record);
118 }
119
120 return $record;
121 }
122
123 /**
124 * Items loading implementation.
125 *
126 * @param array $clauses List of associative columns to fetch
127 * (column => [operator, value])
128 * @param int $start Query limit start.
129 * @param int $lim Query limit value.
130 *
131 * @return array List of record objects.
132 */
133 public function getItems(array $clauses = [], $start = 0, $lim = 0)
134 {
135 $dbo = JFactory::getDbo();
136
137 $q = $dbo->getQuery(true)
138 ->select('*')
139 ->from($dbo->qn('#__vikbooking_payschedules'));
140
141 foreach ($clauses as $column => $data) {
142 if (!is_array($data) || !isset($data['value'])) {
143 continue;
144 }
145 $q->where($dbo->qn($column) . ' ' . ($data['operator'] ?? '=') . ' ' . $dbo->q($data['value']));
146 }
147
148 $q->order($dbo->qn('status') . ' ASC');
149 $q->order($dbo->qn('fordt') . ' ASC');
150
151 $dbo->setQuery($q, $start, $lim);
152
153 $records = $dbo->loadObjectList();
154
155 return array_map([$this, 'normalizeObject'], $records);
156 }
157
158 /**
159 * Method to delete one or more records.
160 *
161 * @param mixed $pks An array of record primary keys, or a single one.
162 *
163 * @return bool True if successful, false if an error occurs.
164 *
165 * @since 1.8.19 (J) - 1.8.9 (WP)
166 */
167 public function delete($pks)
168 {
169 $dbo = JFactory::getDbo();
170
171 if (!$pks) {
172 // nothing to delete
173 return false;
174 }
175
176 if (!is_array($pks)) {
177 // wrap into an array
178 $pks = [$pks];
179 }
180
181 $deleted = 0;
182
183 foreach ($pks as $pk) {
184 // ensure the item exists
185 $item = $this->getItem($pk);
186
187 if (!$item) {
188 continue;
189 }
190
191 $dbo->setQuery(
192 $dbo->getQuery(true)
193 ->delete($dbo->qn('#__vikbooking_payschedules'))
194 ->where($dbo->qn('id') . ' = ' . (int) $item->id)
195 );
196 $dbo->execute();
197
198 if ((bool) $dbo->getAffectedRows()) {
199 $deleted++;
200 }
201 }
202
203 return (bool) $deleted;
204 }
205
206 /**
207 * Watches and eventually processes the automatic payment collections scheduled.
208 *
209 * @param int $lim the limit of payments to process, defaults to 5 per execution.
210 *
211 * @return int number of payments processed, where -1 indicates no running.
212 */
213 public function watch($lim = 5)
214 {
215 $dbo = JFactory::getDbo();
216
217 // number of schedules processed
218 $processed = 0;
219
220 // build date object intervals
221 $now_dt = JFactory::getDate('now', new DateTimeZone(date_default_timezone_get()));
222 $yesterday_dt = (clone $now_dt)->modify('-1 day');
223
224 // fetch unprocessed payments scheduled within the last 24 hours
225 $q = $dbo->getQuery(true)
226 ->select('*')
227 ->from($dbo->qn('#__vikbooking_payschedules'))
228 ->where($dbo->qn('fordt') . ' >= ' . $dbo->q($yesterday_dt->toSql(true)))
229 ->where($dbo->qn('fordt') . ' <= ' . $dbo->q($now_dt->toSql(true)))
230 ->where($dbo->qn('status') . ' = 0');
231
232 $dbo->setQuery($q, 0, $lim);
233 $payschedules = $dbo->loadObjectList();
234
235 foreach ($payschedules as $payschedule) {
236 // immediately update the record status to processed (1)
237 $dbo->setQuery(
238 $dbo->getQuery(true)
239 ->update($dbo->qn('#__vikbooking_payschedules'))
240 ->set($dbo->qn('status') . ' = 1')
241 ->where($dbo->qn('id') . ' = ' . (int) $payschedule->id)
242 );
243 $dbo->execute();
244
245 // process the automatic payment
246 try {
247 if ($this->processPaySchedule($payschedule)) {
248 // increase counter
249 $processed++;
250 }
251 } catch (Exception $e) {
252 // append failure execution log and update status (2 = error)
253 $maxBytes = 65000;
254 $recordLog = ltrim($payschedule->logs . "\n" . $e->getMessage(), "\n");
255 if (strlen($recordLog) > $maxBytes) {
256 $recordLog = function_exists('mb_strcut') ? mb_strcut($recordLog, -$maxBytes, $maxBytes, 'UTF-8') : substr($recordLog, -$maxBytes, $maxBytes);
257 }
258 $dbo->setQuery(
259 $dbo->getQuery(true)
260 ->update($dbo->qn('#__vikbooking_payschedules'))
261 ->set($dbo->qn('logs') . ' = ' . $dbo->q($recordLog))
262 ->set($dbo->qn('status') . ' = 2')
263 ->where($dbo->qn('id') . ' = ' . (int) $payschedule->id)
264 );
265 $dbo->execute();
266 }
267 }
268
269 return $processed;
270 }
271
272 /**
273 * Parses a payment schedule object to normalize some of its properties.
274 *
275 * @param object $payschedule The payment schedule record object.
276 *
277 * @return object
278 */
279 protected function normalizeObject($payschedule)
280 {
281 $now_dt = JFactory::getDate('now');
282 $target_dt = JFactory::getDate($payschedule->fordt);
283
284 $payschedule->time_hm = $target_dt->format('H:i');
285 $payschedule->dt_diff = VBORemindersHelper::getInstance()->relativeDatesDiff($target_dt, $now_dt);
286
287 return $payschedule;
288 }
289
290 /**
291 * Processes the scheduled automatic payment object by attempting
292 * to collect the money from the CC/VCC and by updating all data.
293 *
294 * @param object $payschedule The payment schedule record object.
295 *
296 * @return bool
297 *
298 * @throws Exception
299 */
300 protected function processPaySchedule($payschedule)
301 {
302 $dbo = JFactory::getDbo();
303
304 $booking_info = VikBooking::getBookingInfoFromID($payschedule->idorder);
305 if (!$booking_info) {
306 throw new Exception('Reservation not found.', 404);
307 }
308
309 // currency code
310 $currency_code = !empty($booking_info['chcurrency']) ? $booking_info['chcurrency'] : VikBooking::getCurrencyName();
311 if (empty($currency_code) || strlen((string) $currency_code) != 3) {
312 // fallback to currency transaction code
313 $currency_code = VikBooking::getCurrencyCodePp();
314 }
315
316 // access the reservation model
317 $model = VBOModelReservation::getInstance($booking_info, true);
318
319 try {
320 // get the card details associated with the booking
321 $card = $model->getCardValuePairs();
322
323 if (!$card) {
324 throw new Exception('No credit card details found for the reservation.', 500);
325 }
326
327 // set transaction values within the card array
328 $card['currency'] = $currency_code;
329 $card['amount'] = $payschedule->amount;
330 $card['cardholder'] = $card['cardholder'] ?? $card['name'] ?? null;
331 $card['expiry'] = $card['expiry'] ?? $card['expiration_date'] ?? null;
332
333 // get the payment processor with the card found
334 $processor = $model->getPaymentProcessor($card);
335 } catch (Exception $e) {
336 // propagate the error
337 throw $e;
338 }
339
340 if (!method_exists($processor, 'isDirectChargeSupported') || !$processor->isDirectChargeSupported()) {
341 throw new Exception('The payment method does not allow to directly charge credit cards.', 500);
342 }
343
344 // get the processor name
345 $payment_name = $model->getPaymentName();
346
347 // default transaction response
348 $array_result = [
349 'verified' => 0,
350 ];
351
352 try {
353 // perform the transaction
354 $array_result = $processor->directCharge();
355 } catch (Exception $e) {
356 // set error message
357 $array_result['log'] = sprintf(JText::translate('VBO_CC_TN_ERROR') . " \n%s", $e->getMessage());
358 }
359
360 if ($array_result['verified'] != 1) {
361 // erroneous response
362 if (!empty($array_result['log']) && is_string($array_result['log'])) {
363 throw new Exception($array_result['log'], 500);
364 } else {
365 throw new Exception('Operation failed.', 500);
366 }
367 }
368
369 // valid transaction response!
370 // update booking details
371
372 // get the amount paid
373 $tn_amount = isset($array_result['tot_paid']) ? (float) $array_result['tot_paid'] : null;
374
375 // get the log string, if any
376 $tn_log = !empty($array_result['log']) ? $array_result['log'] : '';
377
378 // update record
379 $upd_record = new stdClass;
380 $upd_record->id = $booking_info['id'];
381 if ($tn_amount) {
382 // update amount paid
383 $upd_record->totpaid = $booking_info['totpaid'] + $tn_amount;
384 // update payable amount (if needed)
385 $new_payable = $booking_info['payable'] - $tn_amount;
386 $new_payable = $new_payable < 0 ? 0 : $new_payable;
387 $upd_record->payable = $new_payable;
388 }
389 if ($tn_log) {
390 $upd_record->paymentlog = $booking_info['paymentlog'] . "\n\n" . date('c') . "\n" . $tn_log;
391 }
392 $upd_record->paymcount = ((int) $booking_info['paymcount'] + 1);
393
394 // update reservation record
395 $dbo->updateObject('#__vikbooking_orders', $upd_record, 'id');
396
397 // payment processor name
398 $pay_process_name = $payment_name ?: 'CC Scheduled Charge';
399
400 // handle transaction data to eventually support a later transaction of type refund
401 $tn_data = isset($array_result['transaction']) ? $array_result['transaction'] : null;
402 if ($tn_amount) {
403 // check event data payload to store
404 if (is_array($tn_data)) {
405 // set key
406 $tn_data['amount_paid'] = $tn_amount;
407 } elseif (is_object($tn_data)) {
408 // set property
409 $tn_data->amount_paid = $tn_amount;
410 } elseif (!$tn_data) {
411 // build an array (we add the payment name because we know there is no other transaction data)
412 $tn_data = [
413 'amount_paid' => $tn_amount,
414 'payment_method' => $pay_process_name,
415 ];
416 }
417 }
418
419 /**
420 * Check if the payment processor returned the information about the amount of processing fees.
421 */
422 if ($tn_data && isset($array_result['tot_fees']) && $array_result['tot_fees']) {
423 // check event data payload to store
424 if (is_array($tn_data)) {
425 // set key
426 $tn_data['processing_fees'] = (float)$array_result['tot_fees'];
427 } elseif (is_object($tn_data)) {
428 // set property
429 $tn_data->processing_fees = (float)$array_result['tot_fees'];
430 }
431 }
432
433 // add an extra data to identify the transaction as automatic, from a schedule
434 if ($tn_data) {
435 if (is_array($tn_data)) {
436 // set key
437 $tn_data['pay_schedule'] = $payschedule->id ?? 1;
438 } elseif (is_object($tn_data)) {
439 // set property
440 $tn_data->pay_schedule = $payschedule->id ?? 1;
441 }
442 }
443
444 // Booking History
445 $main_descr = JText::translate('VBO_AUTOPAY_SCHEDULED');
446 $main_descr = $main_descr != 'VBO_AUTOPAY_SCHEDULED' ? $main_descr : 'Automatic payment collection';
447 $ev_descr = "{$main_descr} - {$pay_process_name}";
448 VikBooking::getBookingHistoryInstance()->setBid($booking_info['id'])->setExtraData($tn_data)->store('P' . ($booking_info['paymcount'] > 0 ? 'N' : '0'), $ev_descr);
449
450 return true;
451 }
452 }
453