PluginProbe
VikBooking Hotel Booking Engine & PMS / 1.8.8
VikBooking Hotel Booking Engine & PMS v1.8.8
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.8, at admin/helpers/src/model/quote.php

421 lines 16.4 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.idquote'),
141 $dbo->qn('or.idroom'),
142 $dbo->qn('or.adults'),
143 $dbo->qn('or.children'),
144 $dbo->qn('or.pets'),
145 $dbo->qn('or.idtar'),
146 $dbo->qn('or.optionals'),
147 $dbo->qn('or.roomindex'),
148 $dbo->qn('or.cust_cost'),
149 $dbo->qn('or.cust_idiva'),
150 $dbo->qn('or.cust_cpolicy_id'),
151 $dbo->qn('or.extracosts'),
152 $dbo->qn('or.room_cost'),
153 ])
154 ->from($dbo->qn('#__vikbooking_orders', 'o'))
155 ->leftJoin($dbo->qn('#__vikbooking_ordersrooms', 'or') . ' ON ' . $dbo->qn('or.idorder') . ' = ' . $dbo->qn('o.id'))
156 ->where($dbo->qn('o.idquote') . ' IN (' . implode(', ', array_map('intval', $quoteIds)) . ')')
157 ->order($dbo->qn('o.idquote') . ' ASC')
158 ->order($dbo->qn('o.id') . ' ASC')
159 ->order($dbo->qn('or.id') . ' ASC')
160 );
161
162 $bookingRooms = $dbo->loadObjectList();
163
164 // pre-cache all involved rooms data
165 $roomsData = VikBooking::getAvailabilityInstance()->loadRooms(array_values(array_unique(array_column($bookingRooms, 'idroom'))), 0, true);
166
167 // scan all quote records
168 foreach ($quotes as &$quote) {
169 // build quote solutions
170 $quote->solutions = [];
171
172 // get all booking room records for the current quote
173 $quoteBookings = array_values(array_filter($bookingRooms, function($bookingRoom) use ($quote) {
174 return $bookingRoom->idquote == $quote->id;
175 }));
176
177 // obtain a unique list of booking IDs (solutions) involved
178 $bookingIds = array_values(array_unique(array_column($quoteBookings, 'id')));
179
180 // scan all booking solutions
181 foreach ($bookingIds as $bookingId) {
182 // get all booking rooms data
183 $bookingRoomsData = array_values(array_filter($quoteBookings, function($bookingRoom) use ($bookingId) {
184 return $bookingRoom->id == $bookingId;
185 }));
186
187 // build quote solution rooms
188 $quoteSolutionRooms = [];
189 foreach ($bookingRoomsData as $bookingRoomData) {
190 // push booking room data
191 $quoteSolutionRooms[] = (object) [
192 'id' => $bookingRoomData->idroom,
193 'name' => $roomsData[$bookingRoomData->idroom]['name'] ?? $bookingRoomData->idroom,
194 'img' => $roomsData[$bookingRoomData->idroom]['img'] ?? null,
195 'adults' => $bookingRoomData->adults,
196 'children' => $bookingRoomData->children,
197 'pets' => $bookingRoomData->pets,
198 'idtar' => $bookingRoomData->idtar,
199 'optionals' => $bookingRoomData->optionals,
200 'roomindex' => $bookingRoomData->roomindex,
201 'cust_cost' => $bookingRoomData->cust_cost,
202 'cust_idiva' => $bookingRoomData->cust_idiva,
203 'cust_cpolicy_id' => $bookingRoomData->cust_cpolicy_id,
204 'extracosts' => $bookingRoomData->extracosts ? (array) json_decode($bookingRoomData->extracosts, true) : [],
205 'room_cost' => $bookingRoomData->room_cost,
206 ];
207 }
208
209 // build quote booking solution
210 $quoteSolution = (object) [
211 'id' => $bookingId,
212 'checkin' => $bookingRoomsData[0]->checkin,
213 'checkout' => $bookingRoomsData[0]->checkout,
214 'nights' => $bookingRoomsData[0]->days,
215 'status' => $bookingRoomsData[0]->status,
216 'total' => $bookingRoomsData[0]->total,
217 'idpayment' => $bookingRoomsData[0]->idpayment,
218 'sid' => $bookingRoomsData[0]->sid,
219 'ts' => $bookingRoomsData[0]->ts,
220 'rooms' => $quoteSolutionRooms,
221 ];
222
223 // push quote solution
224 $quote->solutions[] = $quoteSolution;
225 }
226 }
227
228 // unset last reference even if we are dealing with objects
229 unset($quote);
230 }
231
232 // return the list of quote records
233 return $quotes;
234 }
235
236 /**
237 * Counts the last records found.
238 *
239 * @return int
240 */
241 public function countRecordsFound()
242 {
243 return $this->lastRecordsFound;
244 }
245
246 /**
247 * Should be called after a booking that belongs to a quote gets confirmed.
248 * In case of multiple booking solutions, releases the previously locked
249 * rooms that were included in other booking solutions.
250 *
251 * @param int $quoteId The quote identifier.
252 * @param ?int $confirmedId Optional booking ID that was confirmed.
253 *
254 * @return bool True if any other booking solutions were involved for release.
255 */
256 public function releaseUnconfirmedSolutions(int $quoteId, ?int $confirmedId = null)
257 {
258 $dbo = JFactory::getDbo();
259
260 $dbo->setQuery(
261 $dbo->getQuery(true)
262 ->select($dbo->qn('id'))
263 ->from($dbo->qn('#__vikbooking_orders'))
264 ->where($dbo->qn('idquote') . ' = ' . $quoteId)
265 ->where($dbo->qn('status') . ' != ' . $dbo->q('confirmed'))
266 ->where($dbo->qn('id') . ' != ' . (int) $confirmedId)
267 );
268
269 $releaseBookingIds = array_map('intval', array_column($dbo->loadAssocList(), 'id'));
270
271 // access temp-lock model
272 $tmpLockModel = VBOMvcModel::getInstance('tmplock');
273
274 // scan all booking IDs to release, if any
275 foreach ($releaseBookingIds as $releaseBookingId) {
276 // delete any previously locked record for this booking
277 $tmpLockModel->deleteFromBooking($releaseBookingId);
278 }
279
280 // check whether the CM may have locked rooms on OTAs temporarily
281 if ($releaseBookingIds && method_exists('VCMRequestAvailability', 'setForRelease')) {
282 $vcmAv = VCMRequestAvailability::getInstance();
283 // let the CM schedule the release of the involved and unconfirmed booking IDs, if needed
284 $vcmAv->setForRelease($releaseBookingIds);
285 if ($confirmedId) {
286 // delete any release schedule for the confirmed reservation
287 $vcmAv->deleteFromBooking($confirmedId);
288 }
289 }
290
291 return !empty($releaseBookingIds);
292 }
293
294 /**
295 * Returns a list of booking IDs assigned to the given quote ID.
296 *
297 * @param int $quoteId The quote identifier.
298 *
299 * @return array List of related booking IDs or empty array.
300 */
301 public function getRelatedBookingIds(int $quoteId)
302 {
303 $dbo = JFactory::getDbo();
304
305 $dbo->setQuery(
306 $dbo->getQuery(true)
307 ->select($dbo->qn('id'))
308 ->from($dbo->qn('#__vikbooking_orders'))
309 ->where($dbo->qn('idquote') . ' = ' . $quoteId)
310 );
311
312 return array_map('intval', array_column($dbo->loadAssocList(), 'id'));
313 }
314
315 /**
316 * Sends the quotation details via email to the customer.
317 *
318 * @param int $quoteId The quote identifier.
319 *
320 * @return true
321 *
322 * @throws Exception
323 */
324 public function sendEmail(int $quoteId)
325 {
326 // get record
327 $quote = $this->getItem($quoteId);
328
329 if (!$quote) {
330 throw new Exception('Could not send quote via email: quote ID not found.', 404);
331 }
332
333 if (empty($quote->email)) {
334 throw new Exception('Could not send quote via email: quote customer email address is empty.', 400);
335 }
336
337 if (empty($quote->message)) {
338 throw new Exception('Could not send quote via email: quote message is empty.', 400);
339 }
340
341 // get sender e-mail
342 $adsendermail = VBOFactory::getConfig()->get('senderemail');
343
344 // init mail data
345 $mail = new VBOMailWrapper([
346 'sender' => [$adsendermail, VikBooking::getFrontTitle()],
347 'recipient' => $quote->email,
348 'bcc' => VikBooking::addAdminEmailRecipient(null, true),
349 'reply' => $adsendermail,
350 'subject' => $quote->subject ?: sprintf('%s - %s', VikBooking::getFrontTitle(), JText::translate('VBO_QUOTE_DETAILS')),
351 'content' => $this->parseMessageContent($quote),
352 ]);
353
354 /**
355 * Trigger event to allow third party plugins to overwrite any aspect of the mail message.
356 */
357 VBOFactory::getPlatform()->getDispatcher()->trigger('onBeforeSendQuoteMail', [$mail]);
358
359 // send e-mail
360 if (!VBOFactory::getPlatform()->getMailer()->send($mail)) {
361 // throw an error
362 throw new Exception('Sending the email message to the customer failed.', 500);
363 }
364
365 // update "sent" flag
366 $this->save([
367 'id' => $quote->id,
368 'sent' => 1,
369 ]);
370
371 // always return true on success
372 return true;
373 }
374
375 /**
376 * Parses the content of the quote message.
377 *
378 * @param object $quote The quote record.
379 *
380 * @return string
381 */
382 public function parseMessageContent(object $quote)
383 {
384 // get raw message content
385 $message = $quote->message ?? '';
386
387 // get all quote related booking IDs
388 $bookingIds = $this->getRelatedBookingIds($quote->id);
389
390 // access booking registry for the first related booking (if any)
391 $bookingRegistry = $bookingIds ? VBOBookingRegistry::getInstance(['id' => $bookingIds[0] ?? 0]) : null;
392
393 // build the list of known tag parameters
394 $tagParameters = [
395 '{first_name}' => $quote->first_name ?? '',
396 '{last_name}' => $quote->last_name ?? '',
397 '{checkin_date}' => $bookingRegistry ? VikBooking::formatDateTs($bookingRegistry->getStayTimestamps()[0]) : '----',
398 '{checkout_date}' => $bookingRegistry ? VikBooking::formatDateTs($bookingRegistry->getStayTimestamps()[1]) : '----',
399 '{num_nights}' => $bookingRegistry ? $bookingRegistry->getTotalNights() : 0,
400 '{tot_adults}' => $bookingRegistry ? $bookingRegistry->countTotalAdults() : 0,
401 '{tot_children}' => $bookingRegistry ? $bookingRegistry->countTotalChildren() : 0,
402 '{tot_guests}' => $bookingRegistry ? $bookingRegistry->countTotalGuests() : 0,
403 '{quote_link}' => VikBooking::externalroute("index.php?option=com_vikbooking&view=quote&ref={$quote->uuid}", false),
404 ];
405
406 /**
407 * Trigger event to allow third party plugins to manipulate the tag parameters.
408 */
409 VBOFactory::getPlatform()->getDispatcher()->trigger('onParseQuoteMailMessage', [&$tagParameters]);
410
411 // apply replacement values for tag parameters
412 foreach ($tagParameters as $tag => $value) {
413 // replace tag with calculated value
414 $message = str_replace($tag, $value, $message);
415 }
416
417 // return the parsed message content
418 return $message;
419 }
420 }
421