PluginProbe
VikBooking Hotel Booking Engine & PMS / 1.8.9
VikBooking Hotel Booking Engine & PMS v1.8.9
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
vikbooking / admin / helpers / src / model / quote.php

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

430 lines 16.8 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * @package VikBooking
4 * @subpackage core
5 * @author E4J s.r.l.
6 * @copyright Copyright (C) 2026 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 quote model.
16 *
17 * @since 1.18.8 (J) - 1.8.8 (WP)
18 */
19 class VBOModelQuote extends VBOMvcModel
20 {
21 /** @var string */
22 protected $tableName = '#__vikbooking_quotations';
23
24 /** @var int */
25 protected $lastRecordsFound = 0;
26
27 /**
28 * @inheritDoc
29 */
30 protected function preflight(array &$data)
31 {
32 if (empty($data['id'])) {
33 // we are saving a new record
34 do {
35 // always generate a unique secret ID for new records
36 $data['uuid'] = VikBooking::uuid();
37 // repeat in case a record with the same UUID already exists
38 } while ($this->getItem(['uuid' => $data['uuid']]));
39
40 if (empty($data['name'])) {
41 // default to current date-time string
42 $data['name'] = JFactory::getDate('now')->format('Y-m-d H:i:s');
43 }
44
45 // set creation date-time
46 $data['created_on'] = JFactory::getDate('now')->toSql();
47 }
48
49 // always set the IP address
50 $data['ip'] = JFactory::getApplication()->input->server->getString('REMOTE_ADDR', '') ?: null;
51
52 if (empty($data['created_by'])) {
53 // always set a name
54 $user = JFactory::getUser();
55 $data['created_by'] = $user->name ?: 'User';
56 }
57
58 if (!empty($data['valid_until'])) {
59 // convert the provided date-time string into UTC
60 $data['valid_until'] = JFactory::getDate($data['valid_until'], JFactory::getApplication()->get('offset'))->toSql();
61 }
62
63 if (!empty($data['country_3_code']) && strlen($data['country_3_code']) !== 3) {
64 // attempt to convert a country iso2 code into the corresponding iso3 char code
65 $data['country_3_code'] = VikBooking::getCPinInstance()->get3CharCountry($data['country_3_code']);
66 }
67
68 return parent::preflight($data);
69 }
70
71 /**
72 * Loads quote records and related booking information to allow pagination.
73 *
74 * @param int $offset The query offset start.
75 * @param int $limit The query limit.
76 * @param array $options Associative list of optional loading options.
77 *
78 * @return array
79 */
80 public function loadBookingRecords(int $offset = 0, int $limit = 0, array $options = [])
81 {
82 $dbo = JFactory::getDbo();
83
84 // extract filters, if any
85 $filters = (array) ($options['filters'] ?? []);
86
87 // query the quotation records first
88 $q = $dbo->getQuery(true)
89 ->select('SQL_CALC_FOUND_ROWS *')
90 ->from($dbo->qn('#__vikbooking_quotations'));
91
92 foreach ($filters as $column => $filter) {
93 if (is_scalar($filter)) {
94 $q->where($dbo->qn($column) . ' = ' . $dbo->q($filter));
95 } elseif (is_null($filter)) {
96 $q->where($dbo->qn($column) . ' IS NULL');
97 } else {
98 foreach ($filter as $filter_data) {
99 if (!is_array($filter_data) || !isset($filter_data['operand']) || !isset($filter_data['value'])) {
100 continue;
101 }
102 $q->where($dbo->qn($column) . ' ' . $filter_data['operand'] . ' ' . $dbo->q($filter_data['value']));
103 }
104 }
105 }
106
107 $q->order($dbo->qn('created_on') . ' DESC');
108
109 $dbo->setQuery($q, $offset, $limit);
110 $quotes = $dbo->loadObjectList();
111
112 // count the total number of rows found
113 $this->lastRecordsFound = 0;
114 if ($quotes) {
115 $dbo->setQuery(
116 $dbo->getQuery(true)
117 ->select('FOUND_ROWS()')
118 );
119 $this->lastRecordsFound = (int) $dbo->loadResult();
120 }
121
122 // obtain the quote IDs
123 $quoteIds = array_column($quotes, 'id');
124
125 if ($quoteIds) {
126 // query bookings and booking rooms with the involved quotations
127 $dbo->setQuery(
128 $dbo->getQuery(true)
129 ->select([
130 $dbo->qn('o.id'),
131 $dbo->qn('o.ts'),
132 $dbo->qn('o.status'),
133 $dbo->qn('o.days'),
134 $dbo->qn('o.checkin'),
135 $dbo->qn('o.checkout'),
136 $dbo->qn('o.sid'),
137 $dbo->qn('o.idpayment'),
138 $dbo->qn('o.roomsnum'),
139 $dbo->qn('o.total'),
140 $dbo->qn('o.lang'),
141 $dbo->qn('o.idquote'),
142 $dbo->qn('or.idroom'),
143 $dbo->qn('or.adults'),
144 $dbo->qn('or.children'),
145 $dbo->qn('or.pets'),
146 $dbo->qn('or.idtar'),
147 $dbo->qn('or.optionals'),
148 $dbo->qn('or.roomindex'),
149 $dbo->qn('or.cust_cost'),
150 $dbo->qn('or.cust_idiva'),
151 $dbo->qn('or.cust_cpolicy_id'),
152 $dbo->qn('or.extracosts'),
153 $dbo->qn('or.room_cost'),
154 ])
155 ->from($dbo->qn('#__vikbooking_orders', 'o'))
156 ->leftJoin($dbo->qn('#__vikbooking_ordersrooms', 'or') . ' ON ' . $dbo->qn('or.idorder') . ' = ' . $dbo->qn('o.id'))
157 ->where($dbo->qn('o.idquote') . ' IN (' . implode(', ', array_map('intval', $quoteIds)) . ')')
158 ->order($dbo->qn('o.idquote') . ' ASC')
159 ->order($dbo->qn('o.id') . ' ASC')
160 ->order($dbo->qn('or.id') . ' ASC')
161 );
162
163 $bookingRooms = $dbo->loadObjectList();
164
165 // pre-cache all involved rooms data
166 $roomsData = VikBooking::getAvailabilityInstance()->loadRooms(array_values(array_unique(array_column($bookingRooms, 'idroom'))), 0, true);
167
168 // scan all quote records
169 foreach ($quotes as &$quote) {
170 // build quote solutions
171 $quote->solutions = [];
172
173 // get all booking room records for the current quote
174 $quoteBookings = array_values(array_filter($bookingRooms, function($bookingRoom) use ($quote) {
175 return $bookingRoom->idquote == $quote->id;
176 }));
177
178 // obtain a unique list of booking IDs (solutions) involved
179 $bookingIds = array_values(array_unique(array_column($quoteBookings, 'id')));
180
181 // scan all booking solutions
182 foreach ($bookingIds as $bookingId) {
183 // get all booking rooms data
184 $bookingRoomsData = array_values(array_filter($quoteBookings, function($bookingRoom) use ($bookingId) {
185 return $bookingRoom->id == $bookingId;
186 }));
187
188 // build quote solution rooms
189 $quoteSolutionRooms = [];
190 foreach ($bookingRoomsData as $bookingRoomData) {
191 // push booking room data
192 $quoteSolutionRooms[] = (object) [
193 'id' => $bookingRoomData->idroom,
194 'name' => $roomsData[$bookingRoomData->idroom]['name'] ?? $bookingRoomData->idroom,
195 'img' => $roomsData[$bookingRoomData->idroom]['img'] ?? null,
196 'adults' => $bookingRoomData->adults,
197 'children' => $bookingRoomData->children,
198 'pets' => $bookingRoomData->pets,
199 'idtar' => $bookingRoomData->idtar,
200 'optionals' => $bookingRoomData->optionals,
201 'roomindex' => $bookingRoomData->roomindex,
202 'cust_cost' => $bookingRoomData->cust_cost,
203 'cust_idiva' => $bookingRoomData->cust_idiva,
204 'cust_cpolicy_id' => $bookingRoomData->cust_cpolicy_id,
205 'extracosts' => $bookingRoomData->extracosts ? (array) json_decode($bookingRoomData->extracosts, true) : [],
206 'room_cost' => $bookingRoomData->room_cost,
207 ];
208 }
209
210 // build quote booking solution
211 $quoteSolution = (object) [
212 'id' => $bookingId,
213 'checkin' => $bookingRoomsData[0]->checkin,
214 'checkout' => $bookingRoomsData[0]->checkout,
215 'nights' => $bookingRoomsData[0]->days,
216 'status' => $bookingRoomsData[0]->status,
217 'total' => $bookingRoomsData[0]->total,
218 'lang' => $bookingRoomsData[0]->lang,
219 'idpayment' => $bookingRoomsData[0]->idpayment,
220 'sid' => $bookingRoomsData[0]->sid,
221 'ts' => $bookingRoomsData[0]->ts,
222 'rooms' => $quoteSolutionRooms,
223 ];
224
225 // push quote solution
226 $quote->solutions[] = $quoteSolution;
227 }
228 }
229
230 // unset last reference even if we are dealing with objects
231 unset($quote);
232 }
233
234 // return the list of quote records
235 return $quotes;
236 }
237
238 /**
239 * Counts the last records found.
240 *
241 * @return int
242 */
243 public function countRecordsFound()
244 {
245 return $this->lastRecordsFound;
246 }
247
248 /**
249 * Should be called after a booking that belongs to a quote gets confirmed.
250 * In case of multiple booking solutions, releases the previously locked
251 * rooms that were included in other booking solutions.
252 *
253 * @param int $quoteId The quote identifier.
254 * @param ?int $confirmedId Optional booking ID that was confirmed.
255 *
256 * @return bool True if any other booking solutions were involved for release.
257 */
258 public function releaseUnconfirmedSolutions(int $quoteId, ?int $confirmedId = null)
259 {
260 $dbo = JFactory::getDbo();
261
262 $dbo->setQuery(
263 $dbo->getQuery(true)
264 ->select($dbo->qn('id'))
265 ->from($dbo->qn('#__vikbooking_orders'))
266 ->where($dbo->qn('idquote') . ' = ' . $quoteId)
267 ->where($dbo->qn('status') . ' != ' . $dbo->q('confirmed'))
268 ->where($dbo->qn('id') . ' != ' . (int) $confirmedId)
269 );
270
271 $releaseBookingIds = array_map('intval', array_column($dbo->loadAssocList(), 'id'));
272
273 // access temp-lock model
274 $tmpLockModel = VBOMvcModel::getInstance('tmplock');
275
276 // scan all booking IDs to release, if any
277 foreach ($releaseBookingIds as $releaseBookingId) {
278 // delete any previously locked record for this booking
279 $tmpLockModel->deleteFromBooking($releaseBookingId);
280 }
281
282 // check whether the CM may have locked rooms on OTAs temporarily
283 if ($releaseBookingIds && method_exists('VCMRequestAvailability', 'setForRelease')) {
284 $vcmAv = VCMRequestAvailability::getInstance();
285 // let the CM schedule the release of the involved and unconfirmed booking IDs, if needed
286 $vcmAv->setForRelease($releaseBookingIds);
287 if ($confirmedId) {
288 // delete any release schedule for the confirmed reservation
289 $vcmAv->deleteFromBooking($confirmedId);
290 }
291 }
292
293 return !empty($releaseBookingIds);
294 }
295
296 /**
297 * Returns a list of booking IDs assigned to the given quote ID.
298 *
299 * @param int $quoteId The quote identifier.
300 *
301 * @return array List of related booking IDs or empty array.
302 */
303 public function getRelatedBookingIds(int $quoteId)
304 {
305 $dbo = JFactory::getDbo();
306
307 $dbo->setQuery(
308 $dbo->getQuery(true)
309 ->select($dbo->qn('id'))
310 ->from($dbo->qn('#__vikbooking_orders'))
311 ->where($dbo->qn('idquote') . ' = ' . $quoteId)
312 );
313
314 return array_map('intval', array_column($dbo->loadAssocList(), 'id'));
315 }
316
317 /**
318 * Sends the quotation details via email to the customer.
319 *
320 * @param int $quoteId The quote identifier.
321 *
322 * @return true
323 *
324 * @throws Exception
325 */
326 public function sendEmail(int $quoteId)
327 {
328 // get record
329 $quote = $this->getItem($quoteId);
330
331 if (!$quote) {
332 throw new Exception('Could not send quote via email: quote ID not found.', 404);
333 }
334
335 if (empty($quote->email)) {
336 throw new Exception('Could not send quote via email: quote customer email address is empty.', 400);
337 }
338
339 if (empty($quote->message)) {
340 throw new Exception('Could not send quote via email: quote message is empty.', 400);
341 }
342
343 // get sender e-mail
344 $adsendermail = VBOFactory::getConfig()->get('senderemail');
345
346 // init mail data
347 $mail = new VBOMailWrapper([
348 'sender' => [$adsendermail, VikBooking::getFrontTitle()],
349 'recipient' => $quote->email,
350 'bcc' => VikBooking::addAdminEmailRecipient(null, true),
351 'reply' => $adsendermail,
352 'subject' => $quote->subject ?: sprintf('%s - %s', VikBooking::getFrontTitle(), JText::translate('VBO_QUOTE_DETAILS')),
353 'content' => $this->parseMessageContent($quote),
354 ]);
355
356 /**
357 * Trigger event to allow third party plugins to overwrite any aspect of the mail message.
358 */
359 VBOFactory::getPlatform()->getDispatcher()->trigger('onBeforeSendQuoteMail', [$mail]);
360
361 // send e-mail
362 if (!VBOFactory::getPlatform()->getMailer()->send($mail)) {
363 // throw an error
364 throw new Exception('Sending the email message to the customer failed.', 500);
365 }
366
367 // update "sent" flag
368 $this->save([
369 'id' => $quote->id,
370 'sent' => 1,
371 ]);
372
373 // always return true on success
374 return true;
375 }
376
377 /**
378 * Parses the content of the quote message.
379 *
380 * @param object $quote The quote record.
381 *
382 * @return string
383 */
384 public function parseMessageContent(object $quote)
385 {
386 // get raw message content
387 $message = $quote->message ?? '';
388
389 // get all quote related booking IDs
390 $bookingIds = $this->getRelatedBookingIds($quote->id);
391
392 // access booking registry for the first related booking (if any)
393 $bookingRegistry = $bookingIds ? VBOBookingRegistry::getInstance(['id' => $bookingIds[0] ?? 0]) : null;
394
395 // route quote URI
396 $quoteUri = VikBooking::externalroute(
397 "index.php?option=com_vikbooking&view=quote&ref={$quote->uuid}",
398 false,
399 (VikBooking::findProperItemIdType(['quote'], $bookingRegistry->getProperty('lang') ?: null) ?: null)
400 );
401
402 // build the list of known tag parameters
403 $tagParameters = [
404 '{first_name}' => $quote->first_name ?? '',
405 '{last_name}' => $quote->last_name ?? '',
406 '{checkin_date}' => $bookingRegistry ? VikBooking::formatDateTs($bookingRegistry->getStayTimestamps()[0]) : '----',
407 '{checkout_date}' => $bookingRegistry ? VikBooking::formatDateTs($bookingRegistry->getStayTimestamps()[1]) : '----',
408 '{num_nights}' => $bookingRegistry ? $bookingRegistry->getTotalNights() : 0,
409 '{tot_adults}' => $bookingRegistry ? $bookingRegistry->countTotalAdults() : 0,
410 '{tot_children}' => $bookingRegistry ? $bookingRegistry->countTotalChildren() : 0,
411 '{tot_guests}' => $bookingRegistry ? $bookingRegistry->countTotalGuests() : 0,
412 '{quote_link}' => $quoteUri,
413 ];
414
415 /**
416 * Trigger event to allow third party plugins to manipulate the tag parameters.
417 */
418 VBOFactory::getPlatform()->getDispatcher()->trigger('onParseQuoteMailMessage', [&$tagParameters]);
419
420 // apply replacement values for tag parameters
421 foreach ($tagParameters as $tag => $value) {
422 // replace tag with calculated value
423 $message = str_replace($tag, $value, $message);
424 }
425
426 // return the parsed message content
427 return $message;
428 }
429 }
430