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 / quote.php

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

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