diff --git a/modules/qlochannelmanagerconnector/CHANGELOG.txt b/modules/qlochannelmanagerconnector/CHANGELOG.txt deleted file mode 100644 index 3f6eb3a31..000000000 --- a/modules/qlochannelmanagerconnector/CHANGELOG.txt +++ /dev/null @@ -1,12 +0,0 @@ ------------------------------ -# V1.0.0 to V1.0.1 ------------------------------ - -## Improved/Changed Features: - -* [CO] : [#1464] Updated for PHP version 8.1 to 8.4 and MYSql 8 compatibility - -Changelog - -v1.0.0 -- first release diff --git a/modules/qlochannelmanagerconnector/README.md b/modules/qlochannelmanagerconnector/README.md deleted file mode 100644 index 5a2d26d1a..000000000 --- a/modules/qlochannelmanagerconnector/README.md +++ /dev/null @@ -1 +0,0 @@ -## Channel Manager Connector \ No newline at end of file diff --git a/modules/qlochannelmanagerconnector/classes/QcmcChannelManagerBooking.php b/modules/qlochannelmanagerconnector/classes/QcmcChannelManagerBooking.php deleted file mode 100644 index 3454ae263..000000000 --- a/modules/qlochannelmanagerconnector/classes/QcmcChannelManagerBooking.php +++ /dev/null @@ -1,65 +0,0 @@ - 'qcmc_channel_manager_booking', - 'primary' => 'id_channel_manager_booking', - 'fields' => array( - 'id_order' => array('type' => self::TYPE_INT, 'validate' => 'isUnsignedInt'), - 'date_add' => array('type' => self::TYPE_DATE, 'validate' => 'isDate', 'copy_post' => false), - ), - ); - - const QCMC_CHANNEL_MANAGER_IP = '54.216.64.42'; - - /** - * Returns bookings created by api by channel manager - * @param integer $idOrder : send id_order if you want booking of a particular order - * @param string $orderWay : Send order way (DESC|ASC) sorted by date_add - * @return [array of bookings | false] - */ - public static function getChannelManagerBookings($idOrder = 0, $orderWay = 'ASC') - { - $sql = 'SELECT * FROM `'._DB_PREFIX_.'qcmc_channel_manager_booking` WHERE 1'; - - if ($idOrder) { - $sql .= ' AND `id_order` = '.(int)$idOrder; - } - - $sql .= ' ORDER BY `date_add` '.$orderWay; - - if ($result = Db::getInstance()->executeS($sql)) { - if ($idOrder) { - // if needs only one order row then send only one row from the array - $result = $result[0]; - } - } - - return $result; - } -} diff --git a/modules/qlochannelmanagerconnector/qlochannelmanagerconnector.php b/modules/qlochannelmanagerconnector/qlochannelmanagerconnector.php deleted file mode 100644 index 909bd9cbc..000000000 --- a/modules/qlochannelmanagerconnector/qlochannelmanagerconnector.php +++ /dev/null @@ -1,182 +0,0 @@ -name = 'qlochannelmanagerconnector'; - $this->tab = 'analytics_stats'; - $this->version = '1.0.1'; - $this->author = 'Webkul'; - $this->bootstrap = true; - $this->ps_versions_compliancy = array('min' => '1.6', 'max' => '1.6'); - - parent::__construct(); - - $this->displayName = $this->l('Channel Manager Connector'); - $this->description = $this->l('This module checks connection with channel manager and create logs for the bookings sent from channel manager.'); - } - - public function hookDisplayBackOfficeHeader() - { - $this->context->controller->addCSS($this->_path.'views/css/admin/admin_tab_logo.css'); - } - - public function hookDisplayAdminListBefore() - { - // This tpl will only display when at least one booking has come from channel manager - if ('AdminQloappsChannelManagerConnector' == $this->context->controller->controller_name - && QcmcChannelManagerBooking::getChannelManagerBookings() - ) { - return $this->display(__FILE__, 'channel_manager_connection_info.tpl'); - } - } - - public function hookActionAdminControllerSetMedia() - { - // This CSS will only apply when at least one booking has come from channel manager - if ('AdminQloappsChannelManagerConnector' == $this->context->controller->controller_name - && QcmcChannelManagerBooking::getChannelManagerBookings() - ) { - $this->context->controller->addCSS(_MODULE_DIR_.$this->name.'/views/css/hook/wk_cm_connection_info.css'); - } - } - - - public function hookActionValidateOrder($data) - { - // If the order request is coming from channel manager ip then only enter in database - if (Tools::getRemoteAddr() == QcmcChannelManagerBooking::QCMC_CHANNEL_MANAGER_IP) { - $order = $data['order']; - if (!QcmcChannelManagerBooking::getChannelManagerBookings($order->id)) { - $objChannelManagerBooking = new QcmcChannelManagerBooking(); - $objChannelManagerBooking->id_order = $order->id; - $objChannelManagerBooking->save(); - } - } - } - - public function callInstallTab() - { - $this->installTab('AdminQloappsChannelManagerConnector', 'Channel Manager'); - - return true; - } - - public function installTab($class_name, $tab_name, $tab_parent_name = false, $need_tab = true) - { - $tab = new Tab(); - $tab->active = 1; - $tab->class_name = $class_name; - $tab->name = array(); - - foreach (Language::getLanguages(true) as $lang) { - $tab->name[$lang['id_lang']] = $tab_name; - } - - if ($tab_parent_name) { - $tab->id_parent = (int) Tab::getIdFromClassName($tab_parent_name); - } elseif (!$need_tab) { - $tab->id_parent = -1; - } else { - $tab->id_parent = 0; - } - - $tab->module = $this->name; - $res = $tab->add(); - - //Set position Tab - $objTab = new Tab($tab->id); - - // position of the tab will be after hotelreservationsystem module tab - // without calling this function we are getting hotelreservationsystem module tab cache object with old position - ObjectModel::disableCache(); - if (Validate::isLoadedObject( - $objTabForPosition = Tab::getInstanceFromClassName('AdminHotelReservationSystemManagement') - )) { - $objTab->updatePosition(0, ($objTabForPosition->position + 1)); - } else { - $objTab->updatePosition(0, 6); - } - // enable cache variable again - ObjectModel::enableCache(); - - return $res; - } - - public function registerModuleHooks() - { - return $this->registerHook( - array( - 'actionValidateOrder', - 'displayBackOfficeHeader', - 'displayAdminListBefore', - 'actionAdminControllerSetMedia' - ) - ); - } - - public function install() - { - $objModuleDb = new QcmcChannelManagerConnectorDb(); - if (!parent::install() - || !$objModuleDb->createTables() - || !$this->registerModuleHooks() - || !$this->callInstallTab() - ) { - return false; - } - return true; - } - - public function uninstall() - { - $objModuleDb = new QcmcChannelManagerConnectorDb(); - if (!parent::uninstall() - || !$objModuleDb->dropTables() - || !$this->uninstallTab() - ) { - return false; - } - return true; - } - - public function uninstallTab() - { - $moduleTabs = Tab::getCollectionFromModule($this->name); - if (!empty($moduleTabs)) { - foreach ($moduleTabs as $moduleTab) { - $moduleTab->delete(); - } - return true; - } - return false; - } -} diff --git a/modules/qlochannelmanagerconnector/views/css/admin/wk_cm_booking_list.css b/modules/qlochannelmanagerconnector/views/css/admin/wk_cm_booking_list.css deleted file mode 100644 index 9679e476d..000000000 --- a/modules/qlochannelmanagerconnector/views/css/admin/wk_cm_booking_list.css +++ /dev/null @@ -1,52 +0,0 @@ -/** -* NOTICE OF LICENSE -* -* This source file is subject to the Open Software License version 3.0 -* that is bundled with this package in the file LICENSE.md -* It is also available through the world-wide-web at this URL: -* https://opensource.org/license/osl-3-0-php -* If you did not receive a copy of the license and are unable to -* obtain it through the world-wide-web, please send an email -* to support@qloapps.com so we can send you a copy immediately. -* -* DISCLAIMER -* -* Do not edit or add to this file if you wish to upgrade this module to a newer -* versions in the future. If you wish to customize this module for your needs -* please refer to https://store.webkul.com/customisation-guidelines for more information. -* -* @author Webkul IN -* @copyright Since 2010 Webkul -* @license https://opensource.org/license/osl-3-0-php Open Software License version 3.0 -*/ - -#content.bootstrap .panel .panel-heading { - text-transform: none; - height: auto; - margin: -20px -16px 25px -16px; - padding: 15px; - color: #555; - font-size: 16px; - border-bottom: 1px solid #d4d9dd; -} -#form-qcmc_channel_manager_booking .table { - border: 1px solid #ddd; -} -#form-qcmc_channel_manager_booking .table thead>tr>th { - font-weight: bold; - font-size: 13px; - vertical-align: middle; - padding: 15px; - background-color: #ecf6fb; - border-bottom: 1px solid #ddd; -} -#form-qcmc_channel_manager_booking .table tbody>tr>td { - font-size: 13px; - color: #555; - vertical-align: middle; - padding: 15px; - border-bottom: 1px solid #ddd; -} -#form-qcmc_channel_manager_booking .pagination { - font-size: 14px; -} \ No newline at end of file diff --git a/modules/qlochannelmanagerconnector/views/templates/index.php b/modules/qlochannelmanagerconnector/views/templates/index.php deleted file mode 100644 index 58f2264f2..000000000 --- a/modules/qlochannelmanagerconnector/views/templates/index.php +++ /dev/null @@ -1,32 +0,0 @@ -` block, after the last ``: +```smarty +{hook h='displayOrderRoomsBookingsTableHeading' order=$order} +``` + +--- + +#### 2. Row Data Hook — `displayOrderRoomsBookingsTableData` + +**File:** +``` +adminhtl/themes/default/template/controllers/orders/_product_line.tpl +``` +**Line:** 88 (May Vary) + +Add inside the `` block, before ``: +```smarty +{hook h='displayOrderRoomsBookingsTableData' order=$order data=$data} +``` + +--- + + diff --git a/modules/qlocmconnector/classes/QcmcAri.php b/modules/qlocmconnector/classes/QcmcAri.php new file mode 100644 index 000000000..9c24f6601 --- /dev/null +++ b/modules/qlocmconnector/classes/QcmcAri.php @@ -0,0 +1,796 @@ +getPendingAriUpdateRows(); + + if ($pendingRowsToProcess) { + $flatAriUpdateDataPayload = $this->buildAriUpdatePayload($pendingRowsToProcess); + + foreach ($flatAriUpdateDataPayload as $hotelPayload) { + + $hotelId = (int)$hotelPayload['id_pms_property']; + $rowsForHotel = array_filter($pendingRowsToProcess, function ($row) use ($hotelId) { + return (int)$row['id_hotel'] === $hotelId; + }); + + if (empty($rowsForHotel)) { + continue; + } + + $rowIdsForHotel = array_column($rowsForHotel, 'id_ari_updates'); + + $objAriUpdates->markProcessingAriUpdateData($rowIdsForHotel); + + $result = $objChannelManagerApiService->pushARI($hotelPayload); + + if (isset($result['success']) && $result['success']) { + $objAriUpdates->deleteAriUpdateDataByIds($rowIdsForHotel); + } else { + $objAriUpdates->resetAriUpdateDataToPending($rowIdsForHotel); + if (isset($result['http_code']) && $result['http_code']==401) { + + return false; + } + } + } + } + + return true; + } + + //function to build the payload as per pms standard and calculate availability on pms side + private function buildAriUpdatePayload($flatAriUpdateRows) + { + $flatAriUpdatePayloadDateWise = []; + + //build flat items without calling getAvailableRoomsByDate + foreach ($flatAriUpdateRows as $flatAriUpdateRow) { + if (empty($flatAriUpdateRow['ari_data'])) { + continue; + } + + $flatAriUpdateRowData = json_decode($flatAriUpdateRow['ari_data'], true); + + if (!is_array($flatAriUpdateRowData)) { + continue; + } + + $flatAriUpdateItem = [ + 'id_pms_property' => (int)$flatAriUpdateRow['id_hotel'], + 'id_pms_room_type' => (int)$flatAriUpdateRow['id_room_type'], + 'date' => date('Y-m-d', strtotime($flatAriUpdateRow['date'])), + ]; + + //this is just a marker to check later that availabilty must be added + if (array_key_exists('inventory', $flatAriUpdateRowData)) { + $flatAriUpdateItem['need_availability'] = true; + } + + if (array_key_exists('min_booking_offset', $flatAriUpdateRowData)) { + $flatAriUpdateItem['min_booking_offset'] = $flatAriUpdateRowData['min_booking_offset']; + } else { + // Get default min_booking_offset from hotel settings + $flatAriUpdateItem['min_booking_offset'] = HotelOrderRestrictDate::getMinimumBookingOffset( + (int)$flatAriUpdateRow['id_hotel'] + ); + } + + if (array_key_exists('max_booking_offset', $flatAriUpdateRowData)) { + $flatAriUpdateItem['max_booking_offset'] = $flatAriUpdateRowData['max_booking_offset']; + } else { + // Get default max_booking_offset from hotel settings + $flatAriUpdateItem['max_booking_offset'] = HotelOrderRestrictDate::getMaximumCheckoutOffset( + (int)$flatAriUpdateRow['id_hotel'] + ); + } + + // Get LOS (length of stay) restrictions + $flatAriUpdateItem['los'] = $this->getLosRestrictionForDate( + (int)$flatAriUpdateRow['id_room_type'], + $flatAriUpdateRow['date'] + ); + + $flatAriUpdatePayloadDateWise[] = $flatAriUpdateItem; + } + + if (empty($flatAriUpdatePayloadDateWise)) { + return []; + } + + //calculate per-hotel min/max date so we can fetch booking data in one call per hotel + $hotelDateRanges = []; + foreach ($flatAriUpdatePayloadDateWise as &$datewiseAriUpdateData) { + $idHotel = $datewiseAriUpdateData['id_pms_property']; + $idProduct = $datewiseAriUpdateData['id_pms_room_type']; + $date = $datewiseAriUpdateData['date']; + if (!isset($hotelDateRanges[$idHotel])) { + $hotelDateRanges[$idHotel] = ['min' => $date, 'max' => $date]; + } else { + if ($date < $hotelDateRanges[$idHotel]['min']) { + $hotelDateRanges[$idHotel]['min'] = $date; + } + if ($date > $hotelDateRanges[$idHotel]['max']) { + $hotelDateRanges[$idHotel]['max'] = $date; + } + } + + $totalBookingPrice = HotelRoomTypeFeaturePricing::getRoomTypeTotalPrice( + $idProduct, + $datewiseAriUpdateData['date'], + date('Y-m-d H:i:s', strtotime('+1 day', strtotime($datewiseAriUpdateData['date']))), + 1 + ); + + $datewiseAriUpdateData['date_from'] = $datewiseAriUpdateData['date']; + $datewiseAriUpdateData['date_to'] = $datewiseAriUpdateData['date']; + $datewiseAriUpdateData['price_with_tax'] = $totalBookingPrice['total_price_tax_incl']; + $datewiseAriUpdateData['price_without_tax'] = $totalBookingPrice['total_price_tax_excl']; + $datewiseAriUpdateData['tax'] = $totalBookingPrice['total_price_tax_incl'] - $totalBookingPrice['total_price_tax_excl']; + + unset($datewiseAriUpdateData['date']); + } + unset($datewiseAriUpdateData); + + //For each hotel call getBookingData once and build an avialability map + $availabilityMap = []; + + foreach ($hotelDateRanges as $hotelId => $dateRange) { + + $startDate = strtotime($dateRange['min']); + $endDate = strtotime($dateRange['max']); + + // call booking data only one time per hotel + $bookingParams = [ + 'date_from' => date('Y-m-d 00:00:00', $startDate), + 'date_to' => date('Y-m-d 23:59:59', $endDate), + 'hotel_id' => $hotelId, + 'search_available' => 1, + 'search_booked' => 1, + 'search_unavai' => 1, + 'search_partial' => 0, + 'search_cart_rms' => 0, + 'only_search_data' => 1, + ]; + + $objBookingDtl = new HotelBookingDetail(); + $bookingData = $objBookingDtl->getBookingData($bookingParams); + + if (!isset($availabilityMap[$hotelId])) { + $availabilityMap[$hotelId] = []; + } + + if (!empty($bookingData['rm_data'])) { + foreach ($bookingData['rm_data'] as $roomTypeId => $roomTypeData) { + + if (!isset($availabilityMap[$hotelId][$roomTypeId])) { + $availabilityMap[$hotelId][$roomTypeId] = []; + } + + // number of units for this room-type + $numRooms = (int)($roomTypeData['numberOfRooms'] ?? 0); + + $unAvailableRooms = []; + if (!empty($roomTypeData['data']['booked'])) { + foreach ($roomTypeData['data']['booked'] as $bookedRow) { + if (!empty($bookedRow['detail'])) { + foreach ($bookedRow['detail'] as $bookedDetail) { + $unAvailableRooms[] = [ + 'date_from' => strtotime($bookedDetail['date_from']), + 'date_to' => strtotime($bookedDetail['date_to']), + ]; + } + } + } + } + + if (count($roomTypeData['data']['unavailable'])) { + foreach ($roomTypeData['data']['unavailable'] as $unavailableRooms) { + foreach ($unavailableRooms['detail'] as $unavailableDetail) { + if ($unavailableDetail['id_status'] == HotelRoomInformation::STATUS_INACTIVE + ) { + $unAvailableRooms[] = [ + 'date_from' => $startDate, + 'date_to' => $endDate, + ]; + } else if ($unavailableDetail['id_status'] == HotelRoomInformation::STATUS_TEMPORARY_INACTIVE + && $unavailableDetail['date_from'] && $unavailableDetail['date_to'] + ) { + $unAvailableRooms[] = [ + 'date_from' => strtotime($unavailableDetail['date_from']), + 'date_to' => strtotime($unavailableDetail['date_to']), + ]; + } + } + } + } + + for ($date = $startDate; $date <= $endDate; $date = strtotime('+1 day', $date)) { + $dateStr = date('Y-m-d', $date); + + $unAvailableRoomCount = 0; + if (!empty($unAvailableRooms)) { + foreach ($unAvailableRooms as $unAvailableRoom) { + if ($date >= $unAvailableRoom['date_from'] && $date < $unAvailableRoom['date_to']) { + $unAvailableRoomCount++; + } + } + } + + $available = max(0, $numRooms - $unAvailableRoomCount); + $availabilityMap[$hotelId][$roomTypeId][$dateStr] = $available; + } + } + } + } + + foreach ($flatAriUpdatePayloadDateWise as &$payloadItem) { + if (!empty($payloadItem['need_availability'])) { + $available = $availabilityMap[$payloadItem['id_pms_property']][$payloadItem['id_pms_room_type']][$payloadItem['date_to']] ?? 0; + $payloadItem['availability'] = (int)$available; + + unset($payloadItem['need_availability']); + } + } + + unset($payloadItem); + + //sort and merge consecutive dates + usort($flatAriUpdatePayloadDateWise, function($firstCmp, $secondCmp) { + if ($firstCmp['id_pms_property'] !== $secondCmp['id_pms_property']) { + return $firstCmp['id_pms_property'] <=> $secondCmp['id_pms_property']; + } + if ($firstCmp['id_pms_room_type'] !== $secondCmp['id_pms_room_type']) { + return $firstCmp['id_pms_room_type'] <=> $secondCmp['id_pms_room_type']; + } + + return strcmp($firstCmp['date_to'], $secondCmp['date_to']); + }); + + $mergedAriUpdatePayload = []; + $currentData = []; + + foreach ($flatAriUpdatePayloadDateWise as $mergeFlatAriUpdateItem) { + if(empty($currentData)) { + $currentData = $mergeFlatAriUpdateItem; + } else { + if ( + (strtotime($currentData['date_to'] . ' +1 day') == strtotime($mergeFlatAriUpdateItem['date_from'])) + && $currentData['id_pms_property'] == $mergeFlatAriUpdateItem['id_pms_property'] + && $currentData['id_pms_room_type'] == $mergeFlatAriUpdateItem['id_pms_room_type'] + && $currentData['price_with_tax'] == $mergeFlatAriUpdateItem['price_with_tax'] + && $currentData['min_booking_offset'] == $mergeFlatAriUpdateItem['min_booking_offset'] + && $currentData['max_booking_offset'] == $mergeFlatAriUpdateItem['max_booking_offset'] + && $currentData['availability'] == $mergeFlatAriUpdateItem['availability'] + && ( + isset($currentData['los']) && isset($mergeFlatAriUpdateItem['los']) + && $currentData['los']['min_los'] == $mergeFlatAriUpdateItem['los']['min_los'] + && $currentData['los']['max_los'] == $mergeFlatAriUpdateItem['los']['max_los'] + ) && $currentData['price_without_tax'] == $mergeFlatAriUpdateItem['price_without_tax'] + && $currentData['tax'] == $mergeFlatAriUpdateItem['tax'] + ) { + $currentData['date_to'] = $mergeFlatAriUpdateItem['date_to']; + } else { + $mergedAriUpdatePayload[] = $currentData; + $currentData = $mergeFlatAriUpdateItem; + } + } + } + + if ($currentData) { + $mergedAriUpdatePayload[] = $currentData; + } + + $finalAriUpdatePayload = []; + + foreach ($mergedAriUpdatePayload as $row) { + if (!isset($finalAriUpdatePayload[$row['id_pms_property']])) { + $finalAriUpdatePayload[$row['id_pms_property']] = [ + 'id_property' => '', + 'id_pms_property' => (string)$row['id_pms_property'], + 'ari_info' => [] + ]; + } + + // Use LOS restrictions from merged data or get from database + $losRestriction = $row['los'] ?? $this->getLosRestrictionForDate( + (int)$row['id_pms_room_type'], + $row['date_from'] + ); + + // Get booking offset from merged data or use defaults + $minBookingOffset = $row['min_booking_offset'] ?? HotelOrderRestrictDate::getMinimumBookingOffset((int)$row['id_pms_property']); + $maxBookingOffset = $row['max_booking_offset'] ?? HotelOrderRestrictDate::getMaximumCheckoutOffset((int)$row['id_pms_property']); + + $ariInfo = [ + 'id_room_type' => '', + 'id_pms_room_type' => (string)$row['id_pms_room_type'], + 'date_from' => $row['date_from'], + 'date_to' => $row['date_to'], + 'weekdays' => ['sun', 'mon', 'tue', 'wed', 'thu', 'fri', 'sat'], + 'closed' => 0, + 'rates' => [ + json_encode([ + "id_rate_plan"=> "0", + "id_pms_rate_plan" => "0", + 'price_with_tax' => isset($row['price_with_tax']) ? (float)$row['price_with_tax'] : 0, + 'price_without_tax' => isset($row['price_without_tax']) ? (float)$row['price_without_tax'] : 0, + 'tax' => isset($row['tax']) ? (float)$row['tax'] : 0, + "occupancy_based_price" => json_encode([]), + "min_los_arrival" => 0, + "max_los_arrival" => 0, + "min_los_stay" => isset($losRestriction['min_los']) ? (int)$losRestriction['min_los'] : 1, + "max_los_stay" => isset($losRestriction['max_los']) ? (int)$losRestriction['max_los'] : 30, + "stop_sell" => 0, + "close_to_arrival" => 0, + "close_to_departure" => 0, + "min_advanced_booking_offset" => (int)$minBookingOffset, + "max_advanced_booking_offset" => (int)$maxBookingOffset + ]) + ] + ]; + + if (isset($row['availability'])) { + $ariInfo['availability'] = (int)$row['availability']; + } + Hook::exec('actionChannelManagerAriPayloadModifier', array( + 'ariInfo' => &$ariInfo, + 'idHotel' => $row['id_pms_property'], + 'idRoomType' => $row['id_pms_room_type'], + 'dateFrom' => $row['date_from'], + 'dateTo' => $row['date_to'], + )); + $finalAriUpdatePayload[$row['id_pms_property']]['ari_info'][] = $ariInfo; + } + return array_values($finalAriUpdatePayload); + } + + //function to get ari of specific property for channel manager + public function getAriByIdProperty($params) + { + $finalAriUpdatePayload = []; + if($dateWiseHotelAri = $this->getHotelAri($params)) { + if($mergeData = $this->mergeHotelAriData($dateWiseHotelAri)) { + foreach ($mergeData as $idHotel => $productWiseAri){ + foreach ($productWiseAri as $idProduct => $ariData) { + foreach ($ariData as $key => $ari) { + $ariInfo = [ + 'id_room_type' => '', + 'id_pms_room_type' => (string)$idProduct, + 'availability'=> $ari['total_available_rooms'], + 'date_from' => $ari['date_from'], + 'date_to' => $ari['date_to'], + 'weekdays' => ['sun', 'mon', 'tue', 'wed', 'thu', 'fri', 'sat'], + 'closed' => 0, + 'rates' => [ + json_encode([ + "id_rate_plan"=> "0", + "id_pms_rate_plan"=> "0", + 'price_with_tax' => $ari['total_price_with_tax'], + 'price_without_tax' => $ari['total_price_tax_excl'], + "occupancy_based_price" => json_encode([]), + "min_los_arrival"=> 0, + "max_los_arrival"=> 0, + "min_los_stay"=> $ari['min_los'], + "max_los_stay"=> $ari['max_los'], + "stop_sell"=> 0, + "close_to_arrival"=> 0, + "close_to_departure"=> 0, + "min_advanced_booking_offset"=> $ari['minimum_booking_offset'], + "max_advanced_booking_offset"=> $ari['maximum_booking_offset'] + ]) + ], + ]; + + if (!isset($finalAriUpdatePayload[$idHotel])) { + $finalAriUpdatePayload[$idHotel] = [ + 'id_property' => '', + 'id_pms_property' => (string)$idHotel, + 'ari_info' => [] + ]; + } + $finalAriUpdatePayload[$idHotel]['ari_info'][] = $ariInfo; + } + } + } + } + } + + return json_encode(array( + 'success' => true, + 'data' => array_values($finalAriUpdatePayload) + )); + } + + /** + * + * @param array $params Parameters including id_property, id_room_type, date_from, date_to + * @return string JSON encoded ARI response + */ + public function getHotelAri($params) + { + if (!$idProperty = (int)($params['id_property'])) { + return []; + } + + $minimumBookingOffset = HotelOrderRestrictDate::getMinimumBookingOffset($idProperty); + $maximumCheckoutOffset = HotelOrderRestrictDate::getMaximumCheckoutOffset($idProperty); + // Get room type IDs + $idProducts = []; + if (!empty($params['id_room_type'])) { + $idProducts = array_filter(array_map('intval', explode('|', $params['id_room_type']))); + } else { + $objHotelRoomType = new HotelRoomType(); + if ($roomTypesByIdProperty = $objHotelRoomType->getRoomTypeByHotelId($idProperty, Context::getContext()->language->id)) { + foreach ($roomTypesByIdProperty as $roomTypeInfo) { + if ($roomTypeInfo['active']) { + $idProducts[] = $roomTypeInfo['id_room_type']; + } + } + } + } + + if (empty($idProducts)) { + return []; + } + + // Get date range + $dateFrom = !empty($params['date_from']) ? $params['date_from'] : date('Y-m-d'); + $dateTo = !empty($params['date_to']) ? $params['date_to'] : date('Y-m-d', strtotime(HotelOrderRestrictDate::getMaxOrderDate($idProperty))); + + // Build ARI params + $bookingParams = [ + 'date_from' => date('Y-m-d 00:00:00', strtotime($dateFrom)), + 'date_to' => date('Y-m-d 23:59:59', strtotime($dateTo)), + 'hotel_id' => $idProperty, + 'search_available' => 0, + 'search_booked' => 1, + 'search_unavai' => 1, + 'search_partial' => 0, + 'search_cart_rms' => 0, + ]; + + $objBookingDtl = new HotelBookingDetail(); + $searchAriData = []; + $productsLosRestriction = $this->getDateWiseRestrictionOfProducts($idProducts, $dateFrom, $dateTo); + foreach ($idProducts as $idProduct) { + $bookingParams['id_room_type'] = $idProduct; + if(!isset($searchAriData[$idProperty])) { + $searchAriData[$idProperty] = array(); + } + + if(!isset($searchAriData[$idProperty][$idProduct])) { + $searchAriData[$idProperty][$idProduct] = array(); + } + + $bookingData = $objBookingDtl->getBookingData($bookingParams); + + // Calculate date-wise data + for ($currentDate = $dateFrom; $currentDate < $dateTo; $currentDate = date('Y-m-d', strtotime('+1 day', strtotime($currentDate)))) { + $nextDate = date('Y-m-d', strtotime('+1 day', strtotime($currentDate))); + + if (!empty($bookingData['rm_data'])) { + foreach ($bookingData['rm_data'] as $roomType) { + $ariDateInfo = [ + 'date_from' => $currentDate, + 'date_to' => $nextDate, + ]; + + $ariDateInfo['total_available_rooms'] = $roomType['numberOfRooms']; + + // Get all rooms of product(room type) + $rooms = HotelRoomInformation::getHotelRoomsInfo($idProperty, $idProduct); + $numRooms = count($rooms); + $roomDetail = []; + + // Booked rooms + if ($bookingParams['search_booked'] && isset($roomType['data']['booked'])) { + $bookedRoomIds = []; + foreach ($roomType['data']['booked'] as $bookedRow) { + if (!empty($bookedRow['detail'])) { + foreach ($bookedRow['detail'] as $bookDetail) { + if ($currentDate >= date('Y-m-d', strtotime($bookDetail['date_from'])) && $currentDate < date('Y-m-d', strtotime($bookDetail['date_to']))) { + $bookedRoomIds[] = $bookedRow['id_room']; + } + } + } + } + $roomDetail['booked'] = array_values(array_filter($rooms, function($room) use ($bookedRoomIds) { + return in_array($room['id'], $bookedRoomIds); + })); + } + $ariDateInfo['total_available_rooms'] -= count($roomDetail['booked']); + + // Unavailable rooms + if ($bookingParams['search_unavai'] && isset($roomType['data']['unavailable'])) { + $unavailableRoomIds = []; + foreach ($roomType['data']['unavailable'] as $unavailRow) { + if (!empty($unavailRow['detail'])) { + foreach ($unavailRow['detail'] as $unavailDetail) { + if(!$unavailDetail['date_from'] || !$unavailDetail['date_to']) { + $unavailableRoomIds[] = $unavailRow['id_room']; + } else if ($currentDate >= date('Y-m-d', strtotime($unavailDetail['date_from'])) && $currentDate < date('Y-m-d', strtotime($unavailDetail['date_to']))) { + $unavailableRoomIds[] = $unavailRow['id_room']; + } + } + } + } + $roomDetail['unavailable'] = array_values(array_filter($rooms, function($room) use ($unavailableRoomIds) { + return in_array($room['id'], $unavailableRoomIds); + })); + } + $ariDateInfo['total_available_rooms'] -= count($roomDetail['unavailable']); + $totalBookingPrice = HotelRoomTypeFeaturePricing::getRoomTypeTotalPrice( + $idProduct, + $currentDate, + date('Y-m-d H:i:s', strtotime('+1 day', strtotime($currentDate))), + 1 + ); + + if(isset($productsLosRestriction[$idProduct]) + && isset($productsLosRestriction[$idProduct][$currentDate]) + && $productsLosRestriction[$idProduct][$currentDate] + ) { + $ariDateInfo['min_los'] = $productsLosRestriction[$idProduct][$currentDate]['min_los'] ?? 1; + $ariDateInfo['max_los'] = $productsLosRestriction[$idProduct][$currentDate]['max_los'] ?? 30; + } else { + // Use defaults from room type + $objRoomType = new HotelRoomType($idProduct); + $ariDateInfo['min_los'] = (int) $objRoomType->min_los; + $ariDateInfo['max_los'] = (int) $objRoomType->max_los; + } + $ariDateInfo['total_price_with_tax'] = $totalBookingPrice['total_price_tax_incl']; + $ariDateInfo['total_price_tax_excl'] = $totalBookingPrice['total_price_tax_excl']; + $ariDateInfo['tax'] = $totalBookingPrice['total_price_tax_incl'] - $totalBookingPrice['total_price_tax_excl']; + $ariDateInfo['minimum_booking_offset'] = $minimumBookingOffset; + $ariDateInfo['maximum_booking_offset'] = $maximumCheckoutOffset; + $searchAriData[$idProperty][$idProduct][] = $ariDateInfo; + } + } else { + $ariDateInfo = [ + 'date_from' => $currentDate, + 'date_to' => $nextDate, + ]; + $ariDateInfo['total_available_rooms'] = 0; + $totalBookingPrice = HotelRoomTypeFeaturePricing::getRoomTypeTotalPrice( + $idProduct, + $currentDate, + date('Y-m-d H:i:s', strtotime('+1 day', strtotime($currentDate))), + 1 + ); + + if(isset($productsLosRestriction[$idProduct]) + && isset($productsLosRestriction[$idProduct][$currentDate]) + && $productsLosRestriction[$idProduct][$currentDate] + ) { + $ariDateInfo['min_los'] = $productsLosRestriction[$idProduct][$currentDate]['min_los'] ?? 1; + $ariDateInfo['max_los'] = $productsLosRestriction[$idProduct][$currentDate]['max_los'] ?? 30; + } else { + // Use defaults from room type + $objRoomType = new HotelRoomType($idProduct); + $ariDateInfo['min_los'] = (int) $objRoomType->min_los; + $ariDateInfo['max_los'] = (int) $objRoomType->max_los; + } + $ariDateInfo['minimum_booking_offset'] = $minimumBookingOffset; + $ariDateInfo['maximum_booking_offset'] = $maximumCheckoutOffset; + $ariDateInfo['total_price_with_tax'] = $totalBookingPrice['total_price_tax_incl']; + $ariDateInfo['total_price_tax_excl'] = $totalBookingPrice['total_price_tax_excl']; + $ariDateInfo['tax'] = $totalBookingPrice['total_price_tax_incl'] - $totalBookingPrice['total_price_tax_excl']; + $searchAriData[$idProperty][$idProduct][] = $ariDateInfo; + } + } + } + return $searchAriData; + } + + + public function getDateWiseRestrictionOfProducts($idProducts, $dateFrom, $dateTo) + { + $lengthOfStayArr = []; + foreach ($idProducts as $idProduct) { + if(!isset($lengthOfStayArr[$idProduct])) { + $lengthOfStayArr[$idProduct] = []; + } + if (Validate::isLoadedObject($objRoomType = new HotelRoomType($idProduct))) { + $minLosDefault = (int) $objRoomType->min_los; + $maxLosDefault = (int) $objRoomType->max_los; + + $objHotelRoomTypeRestrictionDateRange = new HotelRoomTypeRestrictionDateRange(); + $lengthStayArr = $objHotelRoomTypeRestrictionDateRange->getRoomTypeLengthOfStayRestriction($idProduct); + + // Pre-index restrictions by date for O(1) lookup instead of O(n) per date + $restrictionByDate = []; + foreach ($lengthStayArr as $restriction) { + $dateFromRestriction = $restriction['date_from']; + $dateToRestriction = $restriction['date_to']; + + // Build a date-indexed lookup for fast access + for ($restrDate = $dateFromRestriction; $restrDate <= $dateToRestriction; $restrDate = date('Y-m-d', strtotime('+1 day', strtotime($restrDate)))) { + if (!isset($restrictionByDate[$restrDate])) { + $restrictionByDate[$restrDate] = [ + 'min_los' => isset($restriction['min_los']) && $restriction['min_los'] !== '' ? (int) $restriction['min_los'] : $minLosDefault, + 'max_los' => isset($restriction['max_los']) && $restriction['max_los'] !== '' ? (int) $restriction['max_los'] : $maxLosDefault, + ]; + } + } + } + + // Now iterate through requested date range with O(1) lookup per date + for ($currentDate = $dateFrom; $currentDate < $dateTo; $currentDate = date('Y-m-d', strtotime('+1 day', strtotime($currentDate)))) { + if (isset($restrictionByDate[$currentDate])) { + $lengthOfStayArr[$idProduct][$currentDate] = $restrictionByDate[$currentDate]; + } else { + // Use defaults if no restriction found for this date + $lengthOfStayArr[$idProduct][$currentDate] = [ + 'min_los' => $minLosDefault, + 'max_los' => $maxLosDefault, + ]; + } + } + } + } + + return $lengthOfStayArr; + } + + /** + * Get LOS (Length of Stay) restriction for a specific room type and date + * + * @param int $idRoomType Room type ID + * @param string $date Date in Y-m-d format + * @return array Array with min_los and max_los + */ + public function getLosRestrictionForDate($idRoomType, $date) + { + $result = [ + 'min_los' => 0, + 'max_los' => 0, + ]; + + if (Validate::isLoadedObject($objRoomType = new HotelRoomType($idRoomType))) { + $minLosDefault = (int) $objRoomType->min_los; + $maxLosDefault = (int) $objRoomType->max_los; + + $objHotelRoomTypeRestrictionDateRange = new HotelRoomTypeRestrictionDateRange(); + $lengthStayArr = $objHotelRoomTypeRestrictionDateRange->getRoomTypeLengthOfStayRestriction($idRoomType); + + if (!empty($lengthStayArr)) { + foreach ($lengthStayArr as $restriction) { + $dateFromRestriction = $restriction['date_from']; + $dateToRestriction = $restriction['date_to']; + + // Check if the date falls within the restriction range + if ($date >= $dateFromRestriction && $date <= $dateToRestriction) { + $result['min_los'] = isset($restriction['min_los']) && $restriction['min_los'] !== '' + ? (int) $restriction['min_los'] + : $minLosDefault; + $result['max_los'] = isset($restriction['max_los']) && $restriction['max_los'] !== '' + ? (int) $restriction['max_los'] + : $maxLosDefault; + break; + } + } + } + + // If no restriction found for the date, return defaults + if ($result['min_los'] == 0 && $result['max_los'] == 0) { + $result['min_los'] = $minLosDefault; + $result['max_los'] = $maxLosDefault; + } + } + + return $result; + } + + public function mergeHotelAriData($ariData) + { + $mergedData = []; + foreach ($ariData as $hotelId => $roomTypeData) { + if(!isset($mergedData[$hotelId])) { + $mergedData[$hotelId] = []; + } + + if (!is_array($roomTypeData)) { + continue; + } + + foreach ($roomTypeData as $idRoomType => $roomTypeWiseAri) { + if (!is_array($roomTypeWiseAri) || empty($roomTypeWiseAri)) { + continue; + } + + // Sort by date_from to ensure chronological order + usort($roomTypeWiseAri, function($a, $b) { + return strcmp($a['date_from'], $b['date_from']); + }); + + if(!isset($mergedData[$hotelId][$idRoomType])) { + $mergedData[$hotelId][$idRoomType] = []; + } + + $temp = []; + foreach ($roomTypeWiseAri as $ari) { + if (empty($temp)) { + // First record - initialize temp + $temp = array( + 'date_from' => $ari['date_from'], + 'date_to' => $ari['date_to'], + 'total_available_rooms' => $ari['total_available_rooms'], + 'min_los' => $ari['min_los'], + 'max_los' => $ari['max_los'], + 'total_price_with_tax' => $ari['total_price_with_tax'], + 'total_price_tax_excl' => $ari['total_price_tax_excl'], + 'tax' => $ari['tax'], + 'minimum_booking_offset' => $ari['minimum_booking_offset'], + 'maximum_booking_offset' => $ari['maximum_booking_offset'] + ); + } else { + // Check if consecutive dates (temp.date_to == ari.date_from) AND all other fields match + if ($temp['date_to'] == $ari['date_from'] && + $temp['min_los'] == $ari['min_los'] && + $temp['max_los'] == $ari['max_los'] && + $temp['total_available_rooms'] == $ari['total_available_rooms'] && + $temp['total_price_with_tax'] == $ari['total_price_with_tax'] && + $temp['total_price_tax_excl'] == $ari['total_price_tax_excl'] && + $temp['tax'] == $ari['tax'] && + $temp['minimum_booking_offset'] == $ari['minimum_booking_offset'] && + $temp['maximum_booking_offset'] == $ari['maximum_booking_offset'] + ) { + // Merge - extend date_to + $temp['date_to'] = $ari['date_to']; + } else { + // Not consecutive or different parameters - push temp and start new + $mergedData[$hotelId][$idRoomType][] = $temp; + $temp = array( + 'date_from' => $ari['date_from'], + 'date_to' => $ari['date_to'], + 'total_available_rooms' => $ari['total_available_rooms'], + 'min_los' => $ari['min_los'], + 'max_los' => $ari['max_los'], + 'total_price_with_tax' => $ari['total_price_with_tax'], + 'total_price_tax_excl' => $ari['total_price_tax_excl'], + 'tax' => $ari['tax'], + 'minimum_booking_offset' => $ari['minimum_booking_offset'], + 'maximum_booking_offset' => $ari['maximum_booking_offset'] + ); + } + } + } + + // Push remaining temp data + if (!empty($temp)) { + $mergedData[$hotelId][$idRoomType][] = $temp; + } + } + } + return $mergedData; + } +} diff --git a/modules/qlocmconnector/classes/QcmcAriUpdates.php b/modules/qlocmconnector/classes/QcmcAriUpdates.php new file mode 100644 index 000000000..176f31c56 --- /dev/null +++ b/modules/qlocmconnector/classes/QcmcAriUpdates.php @@ -0,0 +1,172 @@ + 'ari_updates', + 'primary' => 'id_ari_updates', + 'fields' => array( + 'id_hotel' => array('type' => self::TYPE_INT, 'validate' => 'isUnsignedId', 'required' => true), + 'id_room_type' => array('type' => self::TYPE_INT, 'validate' => 'isUnsignedId', 'required' => true), + 'date' => array('type' => self::TYPE_DATE, 'validate' => 'isDate', 'required' => true), + 'ari_data' => array('type' => self::TYPE_STRING, 'validate' => 'isString'), + 'status' => array('type' => self::TYPE_INT, 'validate' => 'isUnsignedInt'), + 'date_add' => array('type' => self::TYPE_DATE, 'validate' => 'isDate'), + 'date_upd' => array('type' => self::TYPE_DATE, 'validate' => 'isDate'), + ), + ); + + //Returns the array of existing ARI update data for a room type, keyed by date + public function getExistingAriUpdateData($idHotel, $idRoomType) + { + $sql = 'SELECT `id_ari_updates` as `id`, `date`, `ari_data` + FROM `'._DB_PREFIX_.'ari_updates` + WHERE id_hotel = '.(int)$idHotel.' + AND id_room_type = '.(int)$idRoomType; + + $existingAriData = Db::getInstance()->executeS($sql); + + // Reindex array by date + $result = []; + if ($existingAriData) { + foreach ($existingAriData as $value) { + $dateKey = date('Y-m-d', strtotime($value['date'])); + $result[$dateKey] = $value; + } + } + return $result; + } + + //Returns the row hotel (order) wise where the queue row is not processed + public function getPendingAriUpdateRows() + { + $sql = 'SELECT * FROM `'._DB_PREFIX_.'ari_updates` + WHERE status = ' . (int) self::ARI_SYNC_NOT_PROCESSED . ' + ORDER BY id_hotel ASC, date_add ASC'; + + return Db::getInstance()->executeS($sql); + } + + //Universal function to save rows in the table, currently handles availablity only + public function saveAriUpdateRow($idHotel, $idRoomType, $dateFrom, $dateTo, $ariData = []) + { + if (empty($idHotel) || empty($idRoomType)) { + return false; + } + + // Set default ari_data if not provided + if(empty($ariData)) { + $ariData['inventory'] = 1; + } + + // Set default ari_data if not provided + $defaultAriData = []; + + // Merge provided data with defaults + $ariData = array_merge($defaultAriData, $ariData); + + // Get existing data for this room type, keyed by date + $getExistingDataByRoomType = $this->getExistingAriUpdateData($idHotel, $idRoomType); + + // for loop from dateFrom to dateTo + for ($date = $dateFrom; $date <= $dateTo; $date = date('Y-m-d', strtotime($date . ' +1 day'))) { + if (isset($getExistingDataByRoomType[$date])) { + // Update existing record + $objAriUpdates = new QcmcAriUpdates($getExistingDataByRoomType[$date]['id']); + $existingAriData = json_decode($getExistingDataByRoomType[$date]['ari_data'], true); + if ($existingAriData) { + $ariData = array_merge($existingAriData, $ariData); + } + } else { + // Create new record + $objAriUpdates = new QcmcAriUpdates(); + } + $objAriUpdates->id_hotel = (int)$idHotel; + $objAriUpdates->id_room_type = (int)$idRoomType; + $objAriUpdates->date = pSQL($date); + $objAriUpdates->ari_data = json_encode($ariData); + $objAriUpdates->status = self::ARI_SYNC_NOT_PROCESSED; + $objAriUpdates->save(); + } + + return true; + } + + //mark the rows in the table under-processing + public function markProcessingAriUpdateData($ids) + { + if (!empty($ids)) { + foreach ($ids as $id) { + $objAriUpdates = new QcmcAriUpdates((int)$id); + if (Validate::isLoadedObject($objAriUpdates)) { + $objAriUpdates->status = self::ARI_SYNC_PROCESSING; + $objAriUpdates->date_upd = date('Y-m-d H:i:s'); + $objAriUpdates->update(); + } + } + } + } + + //Delete the rows which gets successfully processed + public function deleteAriUpdateDataByIds($ids) + { + if (!empty($ids)) { + foreach ($ids as $id) { + $objAriUpdates = new QcmcAriUpdates((int)$id); + if (Validate::isLoadedObject($objAriUpdates)) { + $objAriUpdates->delete(); + } + } + } + } + + //If the response return false then mark all the rows to not processed which were marked as processing + public function resetAriUpdateDataToPending($ids) + { + if (!empty($ids)) { + foreach ($ids as $id) { + $objAriUpdates = new QcmcAriUpdates((int)$id); + + if (Validate::isLoadedObject($objAriUpdates)) { + $objAriUpdates->status = self::ARI_SYNC_NOT_PROCESSED; + $objAriUpdates->date_upd = date('Y-m-d H:i:s'); + $objAriUpdates->update(); + } + } + } + } +} diff --git a/modules/qlocmconnector/classes/QcmcBooking.php b/modules/qlocmconnector/classes/QcmcBooking.php new file mode 100644 index 000000000..fc5d50a2b --- /dev/null +++ b/modules/qlocmconnector/classes/QcmcBooking.php @@ -0,0 +1,308 @@ +getAllOrdersId($params)) { + $objHotelBookingDetail = new HotelBookingDetail(); + $objServiceProductOrderDetail = new ServiceProductOrderDetail(); + foreach ($allOrders as $order) { + $booking = array(); + $booking['id_booking'] = $order['id_order']; + $objOrder = new Order($booking['id_booking']); + $bookingDetails = $objOrder->getProductsDetail(); + + $objOrderState = new OrderState($objOrder->current_state); + $objCustomer = new Customer($objOrder->id_customer); + if($customerAddress = $this->getCustomerAddressByIdCutomer($objOrder->id_customer)) { + if(!Validate::isLoadedObject($customerState = new State($customerAddress['id_state']))) { + $customerState = array(); + } + } else { + $customerAddress = array(); + } + $booking['id_property'] = (int)$params['id_property']; + + if(isset($objOrder->id_channel_manager_booking) && $objOrder->id_channel_manager_booking) { + $booking['id_channel_manager_booking'] = $objOrder->id_channel_manager_booking; + } + + $booking['currency'] = Currency::getCurrency($objOrder->id_currency)['iso_code']; + $booking['booking_status'] = $this->getBookingStatus($objOrderState, $objOrder); + $booking['payment_status'] = ''; + $booking['source'] = $objOrder->source; + $booking['remark'] = ''; + $booking['number_of_rooms'] = ''; + $booking['booking_date'] = $objOrder->date_add; + $booking['modification_date'] = $objOrder->date_upd; + + $booking['guest_detail']['firstname'] = $objCustomer->firstname; + $booking['guest_detail']['lastname'] = $objCustomer->lastname; + $booking['guest_detail']['email'] = $objCustomer->email; + $booking['guest_detail']['phone'] = $objCustomer->phone; + $booking['guest_detail']['address'] = isset($customerAddress['address1']) ? $customerAddress['address1'] : ''; + $booking['guest_detail']['state'] = isset($customerState->name)?$customerState->name : ''; + $booking['guest_detail']['city'] = isset($customerAddress['city']) ? $customerAddress['city'] : ''; + $booking['guest_detail']['zip'] = isset($customerAddress['postcode']) ? $customerAddress['postcode'] : ''; + $booking['guest_detail']['state_code'] = isset($customerState->iso_code)?$customerState->iso_code: ''; + $booking['guest_detail']['country_code'] = isset($customerState->id_country) ? Country::getIsoById($customerState->id_country) : ''; + + $booking['price_details']['total_price_with_tax'] = $objOrder->total_paid_tax_incl; + $booking['price_details']['total_tax'] = Tools::ps_round(($objOrder->total_paid_tax_incl - $objOrder->total_paid_tax_excl), _PS_PRICE_COMPUTE_PRECISION_); + $booking['price_details']['total_paid'] = $objOrder->total_paid_real; + + $roomTypes = array(); + + foreach ($bookingDetails as $bookingRow) { + if ($bookingRow['is_booking_product']) { + foreach ($objHotelBookingDetail->getBookedRoomsByIdOrderDetail($bookingRow['id_order_detail'], $bookingRow['product_id']) as $value) { + $productId = $bookingRow['product_id'] ?? ''; + $dateFrom = !empty($value['date_from']) ? date('Y-m-d', strtotime($value['date_from'])) : ''; + $dateTo = !empty($value['date_to']) ? date('Y-m-d', strtotime($value['date_to'])) : ''; + + $key = $productId . '_' . $dateFrom . '_' . $dateTo; + + + if (!isset($roomTypes[$key])) { + $roomTypes[$key] = array(); + } + + if (!isset($roomTypes[$key]['rooms']) && isset($roomTypes[$key]['rooms'])) { + $roomTypes[$key]['rooms'] = array(); + } + + if (!isset($roomTypes[$key]['total_price_with_tax'])) { + $roomTypes[$key]['total_price_with_tax'] = $value['total_price_tax_incl']; + } else { + $roomTypes[$key]['total_price_with_tax'] += $value['total_price_tax_incl']; + } + + if (!isset($roomTypes[$key]['total_tax'])) { + $roomTypes[$key]['total_tax'] = $value['total_price_tax_excl']; + } else { + $roomTypes[$key]['total_tax'] += $value['total_price_tax_excl']; + } + + $roomTypes[$key]['id_room_type'] = $bookingRow['product_id']; + $roomTypes[$key]['id_rate_plan'] = ''; + $roomTypes[$key]['check_in_date'] = $value['date_from']; + $roomTypes[$key]['check_out_date'] = $value['date_to']; + + $data = array(); + $data['total_price_with_tax'] = $value['total_price_tax_incl']; + $data['total_tax'] = $value['total_price_tax_excl']; + + $data['occupancy'] = array( + 'adults' => $value['adults'], + 'children' => $value['children'], + 'children_ages' => $value['child_ages'], + 'infants' => '', + ); + + $additionalServices = $objServiceProductOrderDetail->getroomTypeServiceProducts( + $objOrder->id, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + null, + 0, + 0, + $value['id'] + ); + + $serviceData = array(); + $additionalServices = array_shift($additionalServices); + if (is_array($additionalServices) && count($additionalServices) && isset($additionalServices['additional_services'])) { + foreach ($additionalServices['additional_services'] as $service) { + $serviceItem = array(); + $serviceItem['name'] = $service['name']; + $serviceItem['quantity'] = $service['quantity']; + $serviceItem['total_price_with_tax'] = $service['total_price_tax_incl']; + $serviceItem['total_tax'] = ($service['total_price_tax_incl'] - $service['total_price_tax_excl']); + + if (WebserviceSpecificManagementCmApi::isPerNightPriceCalculationMethod($service['price_calculation_method'])) { + $serviceItem['price_mode'] = 'per_night'; + } else { + $serviceItem['price_mode'] = 'per_stay'; + } + + $serviceItem['price_per_unit'] = $serviceItem['total_price_with_tax']-$serviceItem['total_tax']; + $serviceItem['taxes'] = $service['product_tax']; + $serviceData[] = $serviceItem; + } + } + + $data['services'] = $serviceData; + $roomTypes[$key]['rooms'][] = $data; + + // initially set number_of_rooms as 0 + if (!isset($roomTypes[$key]['number_of_rooms'])) { + $roomTypes[$key]['number_of_rooms'] = 0; + } + + // if a room is refunded or cancelled then the count will not be sent to the api response + // as we can not change the status room type booking wise for the API response + // Refunds come from completed order returns; the old is_refunded column + // (shops upgraded from 1.7) is no longer written, so it only adds older rooms. + $isRefunded = !empty($value['is_refunded']) + || (method_exists('OrderReturn', 'hasCompletelyRefundedBooking') + && (new OrderReturn())->hasCompletelyRefundedBooking($value['id'])); + if (!$isRefunded + && isset($roomTypes[$key]['number_of_rooms']) + ) { + $roomTypes[$key]['number_of_rooms'] += 1; + } + } + } + } + + $booking['room_bookings'] = array_values($roomTypes); + $bookings[] = $booking; + } + } + return json_encode(array( + 'success' => true, + 'data' => $bookings + )); + } + + private function getAllOrdersId($params = []) + { + if (count($params)) { + $sql = 'SELECT distinct o.`id_order` FROM `'._DB_PREFIX_.'orders` o LEFT JOIN `'._DB_PREFIX_.'htl_booking_detail` hbd ON o.`id_order` = hbd.`id_order` WHERE true '; + + if (isset($params['id_property']) && $params['id_property']) { + $sql .= " AND hbd.`id_hotel` = ".(int)$params['id_property']; + } + + if (isset($params['check_in']) && $params['check_in']) { + if (isset($params['check_in']['gte']) && $params['check_in']['gte']) { + $sql .= " AND hbd.`date_to` >= '".pSQL($params['check_in']['gte'])."'"; + } + } + + if (isset($params['check_in']) && $params['check_in']) { + if (isset($params['check_in']['gt']) && $params['check_in']['gt']) { + $sql .= " AND hbd.`date_to` > '".pSQL($params['check_in']['gt'])."'"; + } + } + + if (isset($params['check_in']) && $params['check_in']) { + if (isset($params['check_in']['lte']) && $params['check_in']['lte']) { + $sql .= " AND hbd.`date_to` <= '".pSQL($params['check_in']['lte'])."'"; + } + } + + if (isset($params['check_in']) && $params['check_in']) { + if (isset($params['check_in']['lt']) && $params['check_in']['lt']) { + $sql .= " AND hbd.`date_to` < '".pSQL($params['check_in']['lt'])."'"; + } + } + + if (isset($params['check_out']) && $params['check_out']) { + if (isset($params['check_out']['gte']) && $params['check_out']['gte']) { + $sql .= " AND hbd.`date_to` >= '".pSQL($params['check_out']['gte'])."'"; + } + } + + if (isset($params['check_out']) && $params['check_out']) { + if (isset($params['check_out']['gt']) && $params['check_out']['gt']) { + $sql .= " AND hbd.`date_to` > '".pSQL($params['check_out']['gt'])."'"; + } + } + + if (isset($params['check_out']) && $params['check_out']) { + if (isset($params['check_out']['lte']) && $params['check_out']['lte']) { + $sql .= " AND hbd.`date_to` <= '".pSQL($params['check_out']['lte'])."'"; + } + } + + if (isset($params['check_out']) && $params['check_out']) { + if (isset($params['check_out']['lt']) && $params['check_out']['lt']) { + $sql .= " AND hbd.`date_to` < '".pSQL($params['check_out']['lt'])."'"; + } + } + + if (isset($params['date_updated']) && $params['date_updated']) { + if (isset($params['date_updated']['gte']) && $params['date_updated']['gte']) { + $sql .= " AND o.`date_upd` >= '".pSQL($params['date_updated']['gte'])."'"; + } + } + + if (isset($params['date_updated']) && $params['date_updated']) { + if (isset($params['date_updated']['gt']) && $params['date_updated']['gt']) { + $sql .= " AND o.`date_upd` > '".pSQL($params['date_updated']['gt'])."'"; + } + } + + if (isset($params['date_updated']) && $params['date_updated']) { + if (isset($params['date_updated']['lte']) && $params['date_updated']['lte']) { + $sql .= " AND o.`date_upd` <= '".pSQL($params['date_updated']['lte'])."'"; + } + } + + if (isset($params['date_updated']) && $params['date_updated']) { + if (isset($params['date_updated']['lt']) && $params['date_updated']['lt']) { + $sql .= " AND o.`date_upd` < '".pSQL($params['date_updated']['lt'])."'"; + } + } + + return Db::getInstance()->executeS($sql); + } else { + return Db::getInstance()->executeS('SELECT `id_order` FROM `'._DB_PREFIX_.'orders` WHERE 1'); + } + } + + private function getCustomerAddressByIdCutomer($idCustomer = 0) + { + return Db::getInstance()->getRow( + 'SELECT * FROM `'._DB_PREFIX_.'address` WHERE id_customer = '.(int)$idCustomer + ); + } + + private function getBookingStatus($objOrderState, $objOrder) + { + $dateAdd = new DateTime($objOrder->date_add); + $dateUpd = new DateTime($objOrder->date_upd); + + $isMoreThanOneMinute = (abs($dateAdd->getTimestamp() - $dateUpd->getTimestamp()) > 60); + + + if (($objOrderState->id == Configuration::get('PS_OS_CANCELED')) || ($objOrderState->id == Configuration::get('PS_OS_REFUND'))) { + return 'cancelled'; + } elseif ($isMoreThanOneMinute) { + return 'modified'; + } else { + return 'new'; + } + } +} diff --git a/modules/qlocmconnector/classes/QcmcCMConnectorDb.php b/modules/qlocmconnector/classes/QcmcCMConnectorDb.php new file mode 100644 index 000000000..8f71734b5 --- /dev/null +++ b/modules/qlocmconnector/classes/QcmcCMConnectorDb.php @@ -0,0 +1,73 @@ +execute(trim($sql))) { + return false; + } + } + return true; + } + + public function dropTables() + { + $sqls = array( + 'DROP TABLE IF EXISTS`'._DB_PREFIX_.'ari_updates`', + "ALTER TABLE `"._DB_PREFIX_."orders` DROP COLUMN `id_channel_manager_booking`", + "ALTER TABLE `" . _DB_PREFIX_ . "htl_booking_detail` DROP COLUMN `rate_plan_name`" + ); + + foreach ($sqls as $sql) { + if (!Db::getInstance()->execute(trim($sql))) { + return false; + } + } + return true; + } +} diff --git a/modules/qlocmconnector/classes/QcmcChannelManagerApiService.php b/modules/qlocmconnector/classes/QcmcChannelManagerApiService.php new file mode 100644 index 000000000..c0c7c4d48 --- /dev/null +++ b/modules/qlocmconnector/classes/QcmcChannelManagerApiService.php @@ -0,0 +1,174 @@ +logDir = $moduleDir . 'logs'; + $this->logFile = $this->logDir . '/' . date('Y-m-d') . '.log'; + $this->baseUrl = 'https://channels.qloapps.com/'; + } + + private function setMethod($method = 'POST') + { + $this->curlConfig['method'] = strtoupper($method); + } + + private function setEndpoint($url) + { + $this->curlConfig['url'] = $this->baseUrl.$url; + } + + private function addHeaders(array $headers) + { + $this->curlConfig['headers'] = array_merge( + $this->curlConfig['headers'] ?? [], + $headers + ); + } + + private function setBody($body) + { + $this->curlConfig['body'] = $body; + } + + private function getResult() + { + $ch = curl_init($this->curlConfig['url']); + + curl_setopt($ch, CURLOPT_CUSTOMREQUEST, $this->curlConfig['method']); + curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); + curl_setopt($ch, CURLOPT_TIMEOUT, 30); + + if (!empty($this->curlConfig['body'])) { + curl_setopt($ch, CURLOPT_POSTFIELDS, $this->curlConfig['body']); + } + + if (!empty($this->curlConfig['headers'])) { + curl_setopt($ch, CURLOPT_HTTPHEADER, $this->curlConfig['headers']); + } + + if ($result = curl_exec($ch)) { + $result = json_decode($result, true); + } else { + $result = []; + } + + $result['http_code'] = curl_getinfo($ch, CURLINFO_HTTP_CODE); + + curl_close($ch); + + if (!is_dir($this->logDir)) { + @mkdir($this->logDir, 0755, true); + } + + $data = date('[Y-m-d H:i e] ') . PHP_EOL; + $data .= 'Channel Manager API called: ' . $this->curlConfig['url'] . PHP_EOL; + if (isset($this->curlConfig['body']) && $this->curlConfig['body']) { + $data .= 'Request : ' . $this->curlConfig['body'] . PHP_EOL; + } + $data .= 'Response : ' . json_encode($result) . PHP_EOL; + $data .= '___________________________________________'; + $data .= PHP_EOL . PHP_EOL; + error_log($data, 3, $this->logFile); + + $this->curlConfig = []; + + return $result; + } + + public function pushARI($payload) + { + $this->setEndpoint('api/v1/push_ari'); + $this->setMethod('POST'); + + $jsonBody = json_encode($payload, JSON_UNESCAPED_SLASHES); + + if ($jsonBody === false) { + return false; + } + + $accessToken = Configuration::get('QCMC_CM_ACCESS_TOKEN'); + + $this->addHeaders([ + 'Content-Type: application/json', + 'X-Requested-With: XMLHttpRequest', + 'X-PMS-REQUEST: true', + 'Authorization: Bearer ' . $accessToken, + ]); + + $this->setBody($jsonBody); + + $result = $this->getResult(); + + return $result; + } + + public function getAccessToken($clientId, $clientSecret) + { + $this->setEndpoint('oauth/token'); + $this->setMethod('POST'); + + $this->addHeaders([ + 'Content-Type: application/x-www-form-urlencoded', + ]); + + $postFields = http_build_query([ + 'grant_type' => 'client_credentials', + 'client_id' => $clientId, + 'client_secret' => $clientSecret, + ]); + + $this->setBody($postFields); + + $response = $this->getResult(); + + return $response; + } + + public function updateAccessToken($accessToken) + { + if(Configuration::updateValue('QCMC_CM_ACCESS_TOKEN', $accessToken)) { + return true; + } + return false; + } + + public static function isAllowedRemoteAddr($remoteAddr) + { + $alwRemoteAddr = array( + '54.216.64.42', + '52.208.202.101', + ); + + return in_array($remoteAddr, $alwRemoteAddr); + } +} diff --git a/modules/qlocmconnector/classes/QcmcChannelManagerOrder.php b/modules/qlocmconnector/classes/QcmcChannelManagerOrder.php new file mode 100644 index 000000000..a8dac582e --- /dev/null +++ b/modules/qlocmconnector/classes/QcmcChannelManagerOrder.php @@ -0,0 +1,70 @@ + Db::getRow() already appends its own "LIMIT 1" — don't add one here. + $sql = 'SELECT 1 FROM `'._DB_PREFIX_.'orders` WHERE '.self::getWhere(); + + return (bool) Db::getInstance()->getValue($sql); + } + + public static function getLastChannelManagerBookingDate() + { + $sql = 'SELECT `date_add` FROM `'._DB_PREFIX_.'orders` WHERE '.self::getWhere().' + ORDER BY `date_add` DESC'; + + return Db::getInstance()->getValue($sql); + } + + /** + * Connected if a booking has come from the channel manager, or the channel manager + * API credentials (client id and client secret) are saved. + * + * @return bool + */ + public static function isChannelManagerConnected() + { + return self::hasChannelManagerBookings() + || (Configuration::get('QCMC_CM_CLIENT_ID') && Configuration::get('QCMC_CM_CLIENT_SECRET')); + } + + protected static function getWhere() + { + return sprintf(self::CM_WHERE, pSQL(Configuration::get('PS_SHOP_DOMAIN'))); + } +} diff --git a/modules/qlocmconnector/classes/QcmcProperty.php b/modules/qlocmconnector/classes/QcmcProperty.php new file mode 100644 index 000000000..fb6d9f128 --- /dev/null +++ b/modules/qlocmconnector/classes/QcmcProperty.php @@ -0,0 +1,60 @@ +getAllHotels(); + $hotels = array(); + + foreach ($allHotels as $hotel) { + $response = array(); + $response['id'] = $hotel['id']; + $response['id_category'] = $hotel['id_category']; + $objHotelBranch = new HotelBranchInformation($response['id'], $this->context->language->id); + $response['name'] = $objHotelBranch->hotel_name; + $response['property_type'] = $this->objModule->l('hotel', 'QcmcChannelManagerProperty'); + $response['description'] = $objHotelBranch->description; + $response['email'] = $objHotelBranch->email; + $response['phone'] = $objHotelBranch->phone; + $response['currency'] = Currency::getCurrency($this->context->currency->id)['iso_code']; + $response['country_code'] = Country::getIsoById($objHotelBranch->id_country); + $response['state'] = State::getNameById($objHotelBranch->id_state); + $response['city'] = $objHotelBranch->city; + $response['address1'] = $objHotelBranch->address; + $response['address2'] = ''; + $response['zip_code'] = $objHotelBranch->zipcode; + $response['latitude'] = $objHotelBranch->latitude; + $response['longitude'] = $objHotelBranch->longitude; + $response['timezone'] = Configuration::get('PS_TIMEZONE'); + $hotels[] = $response; + } + + return json_encode(array( + 'success' => true, + 'data' => $hotels + )); + } +} diff --git a/modules/qlochannelmanagerconnector/classes/QcmcClassInclude.php b/modules/qlocmconnector/classes/QcmcRequiredClasses.php similarity index 67% rename from modules/qlochannelmanagerconnector/classes/QcmcClassInclude.php rename to modules/qlocmconnector/classes/QcmcRequiredClasses.php index c59aab230..f6d27a5c6 100644 --- a/modules/qlochannelmanagerconnector/classes/QcmcClassInclude.php +++ b/modules/qlocmconnector/classes/QcmcRequiredClasses.php @@ -21,5 +21,14 @@ * @license https://opensource.org/license/osl-3-0-php Open Software License version 3.0 */ -require_once _PS_MODULE_DIR_.'qlochannelmanagerconnector/classes/QcmcChannelManagerConnectorDb.php'; -require_once _PS_MODULE_DIR_.'qlochannelmanagerconnector/classes/QcmcChannelManagerBooking.php'; +include_once "QcmcWebService.php"; +include_once "QcmcBooking.php"; +include_once "QcmcProperty.php"; +include_once "QcmcRoomType.php"; +include_once "QcmcChannelManagerApiService.php"; +include_once "QcmcCMConnectorDb.php"; +include_once "QcmcAri.php"; +include_once "QcmcAriUpdates.php"; +include_once "QcmcChannelManagerOrder.php"; +include_once "WebserviceSpecificManagementCmApi.php"; +include_once "QcmcSourceHelper.php"; diff --git a/modules/qlocmconnector/classes/QcmcRoomType.php b/modules/qlocmconnector/classes/QcmcRoomType.php new file mode 100644 index 000000000..b1de5fef4 --- /dev/null +++ b/modules/qlocmconnector/classes/QcmcRoomType.php @@ -0,0 +1,61 @@ +context->language->id); + if ($objHotelBranch->id) { + $objHotelRoomType = new HotelRoomType(); + $allRoomTypes = $objHotelRoomType->getRoomTypeByHotelId($idProperty, $this->context->language->id); + $roomTypes = array(); + + foreach ($allRoomTypes as $room) { + $response = array(); + $response['id'] = $room['id_product']; + $objHotelRoomType = new HotelRoomType($response['id'], $this->context->language->id); + $objHotelRoomInformation = new HotelRoomInformation(); + $response['id_product'] = $room['id_product']; + $response['name'] = $room['room_type']; + $response['total_rooms'] = count($objHotelRoomInformation->getHotelRoomsInfo($idProperty, $response['id'])); + $response['base_occupancy'] = $objHotelRoomType->adults; + $response['max_adults'] = $objHotelRoomType->max_adults; + $response['max_children'] = $objHotelRoomType->max_children; + $response['max_infants'] = $objHotelRoomType->max_children; + $roomTypes[] = $response; + } + + return json_encode(array( + 'success' => true, + 'data' => $roomTypes + )); + } else { + return json_encode(array( + 'success' => false, + 'data' => [], + 'message' => $this->objModule->l('Property not found.', 'QcmcChannelManagerRoomType'), + )); + } + } +} diff --git a/modules/qlocmconnector/classes/QcmcSourceHelper.php b/modules/qlocmconnector/classes/QcmcSourceHelper.php new file mode 100644 index 000000000..90a469872 --- /dev/null +++ b/modules/qlocmconnector/classes/QcmcSourceHelper.php @@ -0,0 +1,181 @@ + [ + 'code' => 'BOOKING_COM', + 'businessSourceCode' => 'OTA', + ], + 'expedia' => [ + 'code' => 'EXPEDIA', + 'businessSourceCode' => 'OTA', + ], + 'goibibo_and_makemytrip' => [ + 'code' => 'GOMMT', + 'businessSourceCode' => 'OTA', + ], + 'agoda' => [ + 'code' => 'AGODA', + 'businessSourceCode' => 'OTA', + ], + 'bakuun' => [ + 'code' => 'BAKUUN', + 'businessSourceCode' => 'OTA', + ], + 'despegar' => [ + 'code' => 'DESPEGAR', + 'businessSourceCode' => 'OTA', + ], + 'yatra' => [ + 'code' => 'YATRA', + 'businessSourceCode' => 'OTA', + ], + 'airbnb' => [ + 'code' => 'AIRBNB', + 'businessSourceCode' => 'OTA', + ], + 'hostelworld' => [ + 'code' => 'HOSTELWORLD', + 'businessSourceCode' => 'OTA', + ], + 'hyperguest' => [ + 'code' => 'HYPERGUEST', + 'businessSourceCode' => 'OTA', + ], + 'trip_com_ctrip' => [ + 'code' => 'TRIP_COM', + 'businessSourceCode' => 'OTA', + ], + 'hotelbeds' => [ + 'code' => 'HOTELBEDS', + 'businessSourceCode' => 'OTA', + ], + ]; + + public function __construct() + { + if (!self::coreClassesExist()) { + throw new Exception('BusinessSource and Source classes are required for QcmcSourceHelper to work.'); + } + } + + public static function coreClassesExist() + { + return class_exists('BusinessSource') && class_exists('Source'); + } + + /** + * Resolves the id_source for a channel-manager booking/business source name, + * creating the BusinessSource/Source rows on first use if they don't exist yet. + */ + public function getBookingSourceId($cmBookingSourceName, $cmBusinessSourceName = null) + { + $key = self::formatSourceKey($cmBookingSourceName); + $bookingSourceCode = isset($this->bookingSourceMapping[$key]) + ? $this->bookingSourceMapping[$key]['code'] + : $this->formatSourceCode($cmBookingSourceName); + + if ($idSource = Source::getIdByCode($bookingSourceCode)) { + return $idSource; + } + + $businessSourceCode = $this->formatSourceCode($cmBusinessSourceName); + $idBusinessSource = $this->getBusinessSourceIdByCode($businessSourceCode) + ?: $this->createBusinessSource($businessSourceCode, $cmBusinessSourceName); + + return $this->createSource($bookingSourceCode, $idBusinessSource, $cmBookingSourceName); + } + + private function getBusinessSourceIdByCode($code) + { + return (int)Db::getInstance()->getValue( + 'SELECT `id_business_source` FROM `'._DB_PREFIX_.'business_source` + WHERE `code` = \''.pSQL($code).'\' AND `deleted` = 0' + ); + } + + private function createBusinessSource($code, $sourceName) + { + $name = array(); + foreach (Language::getLanguages(false) as $lang) { + $name[(int)$lang['id_lang']] = ucwords($sourceName); + } + + $objBusinessSource = new BusinessSource(); + $objBusinessSource->code = $code; + $objBusinessSource->name = $name; + $objBusinessSource->position = BusinessSource::getHigherPosition() + 1; + $objBusinessSource->unremovable = 0; + $objBusinessSource->active = 1; + $objBusinessSource->deleted = 0; + $objBusinessSource->save(); + + return (int)$objBusinessSource->id; + } + + private function createSource($code, $idBusinessSource, $sourceName) + { + $name = array(); + foreach (Language::getLanguages(false) as $lang) { + $name[(int)$lang['id_lang']] = ucwords($sourceName); + } + + $objSource = new Source(); + $objSource->code = $code; + $objSource->id_business_source = $idBusinessSource; + $objSource->name = $name; + $objSource->position = Source::getHigherPosition($idBusinessSource) + 1; + $objSource->unremovable = 0; + $objSource->active = 1; + $objSource->deleted = 0; + $objSource->save(); + + return (int)$objSource->id; + } + + public function getBusinessSourceInfo($cmBookingSourceName) + { + $key = self::formatSourceKey($cmBookingSourceName); + + return $this->bookingSourceMapping[$key] ?? null; + } + + private static function formatSourceKey($sourceName) + { + return trim(preg_replace('/[^a-z0-9]+/', '_', strtolower($sourceName)), '_'); + } + + private static function formatSourceCode($sourceName) + { + return trim(preg_replace('/[^A-Z0-9]+/', '_', strtoupper($sourceName)), '_'); + } +} + + + diff --git a/modules/qlocmconnector/classes/QcmcWebService.php b/modules/qlocmconnector/classes/QcmcWebService.php new file mode 100644 index 000000000..6f0a70387 --- /dev/null +++ b/modules/qlocmconnector/classes/QcmcWebService.php @@ -0,0 +1,310 @@ +objModule = Module::getInstanceByName('qlocmconnector'); + $this->objOutput = $objOutput; + $this->wsObject = $wsObject; + $this->output = $output; + $this->outputFormat = $outputFormat; + $this->setContext(); + } + + public function setContext() + { + $this->context = Context::getContext(); + $this->context->controller = new FrontController(); + $unidentifiedGroupCustomer = new Customer(); + if (Group::isFeatureActive()) { + $unidentifiedGroupCustomer->id_default_group = (int) Configuration::get('PS_UNIDENTIFIED_GROUP'); + } + + // Set Customer + $this->context->customer = $unidentifiedGroupCustomer; + if (isset($this->wsObject->urlFragments['id_customer'])) { + $customer = new Customer($this->wsObject->urlFragments['id_customer']); + if (Validate::isLoadedObject($customer) && $customer->active) { + $this->context->customer = $customer; + $this->context->customer->logged = 1; // Assuming that when getting id_customer, customer is logged in. + } + } else { + if (isset($this->wsObject->urlSegment[2]) && $this->wsObject->urlSegment[2] == 'payment') { + if (isset($this->wsObject->urlSegment[3]) && $this->wsObject->urlSegment[3]) { + $this->context->customer = new Customer($this->wsObject->urlSegment[3]); + } else { + $urlObjCart = new Cart($this->wsObject->urlFragments['id_cart']); + $this->context->customer = new Customer($urlObjCart->id_customer); + } + } + } + + // Set Cart + if (isset($this->wsObject->urlFragments['id_cart'])) { + $this->context->cart = new Cart($this->wsObject->urlFragments['id_cart']); + if (isset($this->wsObject->urlFragments['id_customer']) && !$this->wsObject->urlFragments['id_customer']) { + $this->context->customer = new Customer($this->context->cart->id_customer); + } + } + + // Set language + $language = new Language((int) Configuration::get('PS_LANG_DEFAULT')); + if (isset($this->wsObject->urlFragments['id_lang'])) { + $requestedlanguage = new Language($this->wsObject->urlFragments['id_lang']); + if (Validate::isLoadedObject($requestedlanguage) && $requestedlanguage->active) { + $language = $requestedlanguage; + } + } + + $this->context->language = $language; + // Set Currency + $currency = new Currency((int) Configuration::get('PS_CURRENCY_DEFAULT')); + if (isset($this->wsObject->urlFragments['id_currency'])) { + $requestedCurrency = new Currency($this->wsObject->urlFragments['id_currency']); + if (Validate::isLoadedObject($requestedCurrency) + && $requestedCurrency->active + && !$requestedCurrency->deleted + ) { + $currency = $requestedCurrency; + } + } + + $this->context->currency = $currency; + if (isset($this->wsObject->urlFragments['id_guest']) + && $this->wsObject->urlFragments['id_guest'] + ) { + $objGuest = new Guest((int) $this->wsObject->urlFragments['id_guest']); + $this->context->cookie->id_guest = $objGuest->id; + } + + if (Validate::isLoadedObject($this->context->customer) && $this->context->customer->active) { + $this->context->customer->logged = 1; // hook exec lisy cache build on the basis of groups + if ($customerGuestId = Guest::getFromCustomer((int) $this->context->customer->id)) { + $this->context->customer->id_guest = $customerGuestId; + } + $this->context->customer->save(); + } else { + $this->context->customer = $unidentifiedGroupCustomer; + if (!isset($this->context->cart)) { + $this->context->cart = new Cart(); + } + } + + if (Validate::isLoadedObject($this->context->cart)) { + $this->context->cart->id_currency = $this->context->currency->id; + $this->context->cart->id_lang = $this->context->language->id; + if ($this->context->customer->id) { + $this->context->cart->id_customer = $this->context->customer->id; + if ($customerAddressId = Address::getFirstCustomerAddressId($this->context->customer->id)) { + $this->context->cart->id_address_invoice = $customerAddressId; + } + } + + $this->context->cart->save(); + } + + // to update product $_taxCalculationMethod according to context + Product::getTaxCalculationMethod((int) $this->context->customer->id); + } + + public function renderOutputUsingArray($response, $keyToIgnore = array(), $parentKey = '', $useEmpty = false) + { + $output = ''; + foreach ($response as $key => $res) { + if (in_array($key, $keyToIgnore) && $key) { + continue; + } + + $currentKey = $key; + if (gettype($key) == 'integer' || (int) $key) { + $key = $parentKey; + } + + if (is_array($res) && count($res)) { + $output .= $this->renderHeader($key); + $output .= $this->renderOutputUsingArray($res, $keyToIgnore, $key, $useEmpty); + $output .= $this->renderFooter($key); + } else { + if (empty($res) && !$useEmpty && !in_array($key, array('prefix', 'suffix'))) { + $res = 0; + } + + if (isset($this->wsObject->urlFragments['schema'])) { + if ($this->wsObject->urlFragments['schema'] == 'blank' || $this->wsObject->urlFragments['schema'] == 'synopsis') { + $res = null; + } else { + throw new WebserviceException( + $this->objModule->l('Please select a schema of type \'synopsis\' to get the whole schema informations (which fields are required, which kind of content...) or \'blank\' to get an empty schema to fill before using POST request.', 'QmkWebService'), + array(100, 400) + ); + } + } + + $output .= $this->objOutput->objectRender->renderField( + array( + 'sqlId' => $key, + 'value' => $res + ) + ); + } + } + + return $output; + } + + public function renderHeader($parentKey = '') + { + if ($parentKey != '') { + $this->parent[] = $parentKey; + return $this->objOutput->objectRender->renderNodeHeader($parentKey, + array('objectNodeName' => $parentKey) + ); + } + + return ''; + } + + public function renderFooter($parentKey = '', $multiple = false) + { + if ($parentKey != '') { + array_pop($this->parent); + return $this->objOutput->objectRender->renderNodeFooter($parentKey, + array('objectNodeName' => $parentKey) + ); + } + + return ''; + } + + public function renderResponse($message, $status =null, $args = array(), $parentKey ='', $errors = array(), $root = 'response') + { + $jsonOutput = array(); + $output = ''; + $output .= $this->renderHeader($root); + if ($status !== null) { + $args['status'] = (bool) $status; + } else { + $args['status'] = false; + } + + $args['message'] = $message; + $output .= $this->renderOutputUsingArray($args, array(), $parentKey); + $jsonOutput[$root] = $args; + if (isset($errors) && count($errors) > 0) { + $output .= $this->renderHeader('errors'); + $output .= $this->renderOutputUsingArray($errors, array(), 'error'); + $jsonOutput[$root]['errors'] = $errors['error']; + $output .= $this->renderFooter('errors'); + } + + $output .= $this->renderFooter($root); + if ($this->outputFormat == 'json') { + return json_encode($jsonOutput); + } + + return $output; + } + + + public function resizeImage($url, $name) + { + // @todo: add code for the resized image here... If needed. + // create new url + $newUrl = $url.$name; + + return $this->context->link->getMediaLink($newUrl); + } + + public function renderFormFields($fields, $parentKey ='', $parentKeysToIgnore = array()) + { + $output = ''; + foreach ($fields as $key => $field) { + $currentKey = $key; + if (gettype($key) == 'integer' || (int) $key) { + $key = $parentKey; + } + + if (is_array($field) && count($field)) { + $isParent = false; + if ($key != end($this->parent)) { + $isParent = true; + if (!in_array($key, $parentKeysToIgnore)) { + $output .= $this->renderHeader($key); + } + } + + $output .= $this->renderFormFields($field, $key, $parentKeysToIgnore); + if ($isParent + || $key == 'option' + || $key == 'new_option' + || $key == 'field' + ) { + if ($key == end($this->parent)) { + if (!in_array($key, $parentKeysToIgnore)) { + $output .= $this->renderFooter($key); + } + } + } + } else { + if (isset($this->wsObject->urlFragments['schema'])) { + if ($this->wsObject->urlFragments['schema'] == 'blank' || $this->wsObject->urlFragments['schema'] == 'synopsis') { + $field = null; + } else { + throw new WebserviceException( + $this->objModule->l('Please select a schema of type \'synopsis\' to get the whole schema informations (which fields are required, which kind of content...) or \'blank\' to get an empty schema to fill before using POST request.', 'QmkWebService'), + array(100, 400) + ); + } + } + + if (is_array($field) && !count($field)) { + $field = ''; + } + + $output .= $this->objOutput->objectRender->renderField( + array( + 'sqlId' => $key, + 'value' => $field + ) + ); + } + } + + return $output; + } + +} \ No newline at end of file diff --git a/modules/qlocmconnector/classes/WebserviceSpecificManagementCmApi.php b/modules/qlocmconnector/classes/WebserviceSpecificManagementCmApi.php new file mode 100644 index 000000000..177aa19a9 --- /dev/null +++ b/modules/qlocmconnector/classes/WebserviceSpecificManagementCmApi.php @@ -0,0 +1,5201 @@ + array(), + 'POST' => array() + ); + + public function setObjectOutput(WebserviceOutputBuilderCore $obj) + { + $this->objOutput = $obj; + return $this; + } + + public function getObjectOutput() + { + return $this->objOutput; + } + + public function setWsObject(WebserviceRequestCore $obj) + { + $this->wsObject = $obj; + + return $this; + } + + public function getWsObject() + { + return $this->wsObject; + } + + public function manage() + { + $this->objOutput->setHeaderParams('pms_token', self::PMS_TOKEN); + $this->context = Context::getContext(); + $objQloCMConnector = Module::getInstanceByName('qlocmconnector'); + $objQcmcProperty = new QcmcProperty($this->objOutput, $this->wsObject, $this->output, $this->outputFormat); + $objQcmcRoomType = new QcmcRoomType($this->objOutput, $this->wsObject, $this->output, $this->outputFormat); + $objQcmcBooking = new QcmcBooking($this->objOutput, $this->wsObject, $this->output, $this->outputFormat); + $objAri = new QcmcAri($this->objOutput, $this->wsObject, $this->output, $this->outputFormat); + + switch ($this->wsObject->method) { + case 'GET': + switch ($this->wsObject->urlSegment[1]) { + case 'test_connection': + $this->output = json_encode([ + "success" => true, + "QLO_VERSION" => _QLOAPPS_VERSION_ + ]); + break; + case 'properties': + $this->output = $objQcmcProperty->getHotelList(); + break; + case 'room_types': + if ($this->validateFilters(Tools::getValue('filter')) && isset(Tools::getValue('filter')['id_property'])) { + $this->output = $objQcmcRoomType->getRoomTypeList(Tools::getValue('filter')['id_property']); + } else { + $errorResponse = ["success" => false, "error" => $objQloCMConnector->l('id_property is required!')]; + echo json_encode($errorResponse, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT); + exit; + } + break; + case 'bookings': + $inputData = Tools::getAllValues(); + if (!isset($inputData['filter'])) { + $inputData['filter']['id_property'] = false; + } + + if ($this->validateFilters(Tools::getValue('filter')) && isset(Tools::getValue('filter')['id_property'])) { + $this->output = $objQcmcBooking->getBookingList($inputData['filter']); + } else { + $errorResponse = ["success" => false, "error" => $objQloCMConnector->l('id_property is required!')]; + echo json_encode($errorResponse, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT); + exit; + } + break; + case 'ari': + $inputData = Tools::getAllValues(); + if (!isset($inputData['filter'])) { + $inputData['filter']['id_property'] = false; + } + + if ($this->validateFilters(Tools::getValue('filter')) && isset(Tools::getValue('filter')['id_property'])) { + $this->output = $objAri->getAriByIdProperty($inputData['filter']); + } else { + $errorResponse = ["success" => false, "error" => $objQloCMConnector->l('id_property is required!')]; + echo json_encode($errorResponse, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT); + exit; + } + break; + default: + $errorResponse = ["success" => false, "error" => $objQloCMConnector->l('This HTTP method is not allowed')]; + echo json_encode($errorResponse, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT); + exit; + } + break; + case 'POST': + switch ($this->wsObject->urlSegment[1]) { + case 'booking_notification': + try { + $inputData = $this->getPostRequest(); + $this->id_hotel = $inputData['id_property']; + $this->handleRoomTypePriceWithService($inputData); + $this->processCustomerDetails($inputData); + + // De-dup by cmid: the CM sends one request per room type with the same cmid, + // so a repeated "new" or an unresolved id_booking modifies the existing order. + $cancelledOrderStates = array( + (int) Configuration::get('PS_OS_CANCELED'), + (int) Configuration::get('PS_OS_REFUND'), + ); + $isReversalRequest = isset($inputData['booking_status']) + && in_array($inputData['booking_status'], array(self::API_BOOKING_STATUS_CANCELLED, self::API_BOOKING_STATUS_REFUNDED)); + $objRequestedOrder = !empty($inputData['id_booking']) ? new Order((int) $inputData['id_booking']) : null; + $isRequestedOrderCancelled = $objRequestedOrder + && Validate::isLoadedObject($objRequestedOrder) + && in_array((int) $objRequestedOrder->current_state, $cancelledOrderStates); + + $idExistingOrderByCmid = 0; + if (!empty($inputData['id_channel_manager_booking'])) { + // Same property only (cmid is unique per hotel); non-cancellations match live orders only. + $idExistingOrderByCmid = (int) Db::getInstance()->getValue( + 'SELECT o.`id_order` FROM `'._DB_PREFIX_.'orders` o + WHERE o.`id_channel_manager_booking` = "'.pSQL($inputData['id_channel_manager_booking']).'"' + .($isReversalRequest ? '' : ' AND o.`current_state` NOT IN ('.implode(',', $cancelledOrderStates).')') + .(!empty($inputData['id_property']) + ? ' AND EXISTS (SELECT 1 FROM `'._DB_PREFIX_.'htl_booking_detail` hbd + WHERE hbd.`id_order` = o.`id_order` AND hbd.`id_hotel` = '.(int) $inputData['id_property'].')' + : '').' + ORDER BY o.`id_order` DESC' + ); + } + if ($idExistingOrderByCmid + && (empty($inputData['id_booking']) + || !Validate::isLoadedObject($objRequestedOrder) + || (!$isReversalRequest && $isRequestedOrderCancelled) + ) + ) { + $inputData['id_booking'] = $idExistingOrderByCmid; + } elseif (!$isReversalRequest && $isRequestedOrderCancelled) { + // Re-sent booking whose order is already cancelled here: create it again as a new order. + $inputData['id_booking'] = 0; + } + + $idBookingResolves = !empty($inputData['id_booking']) + && Validate::isLoadedObject(new Order($inputData['id_booking'])); + if (!$idExistingOrderByCmid + && !$idBookingResolves + && $inputData['booking_status'] != self::API_BOOKING_STATUS_NEW + && (!empty($inputData['id_booking']) + || in_array($inputData['booking_status'], array(self::API_BOOKING_STATUS_CANCELLED, self::API_BOOKING_STATUS_REFUNDED)) + ) + ) { + // Non-new request with an unknown id_booking, or a cancel/refund with nothing to cancel: reject. + $errorResponse = ["success" => false, "error" => $objQloCMConnector->l('Referenced booking not found.')]; + echo json_encode($errorResponse, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT); + $this->deleteWsTempObjects(); + exit; + } + + if (!$idExistingOrderByCmid + && ($inputData['booking_status'] == self::API_BOOKING_STATUS_NEW || !$idBookingResolves) + ) { + $inputData['booking_status'] = self::API_BOOKING_STATUS_NEW; + $this->booking_type = self::API_BOOKING_STATUS_NEW; + if ($this->validatePostRequest($inputData)) { + $this->handlePostRequest($inputData); + $this->getResponseJson(); + } else { + $errorResponse = ["success" => false, "error" => $objQloCMConnector->l($this->error_msg)]; + echo json_encode($errorResponse, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT); + $this->deleteWsTempObjects(); + exit; + } + } else { + if ($this->validatePutRequest($inputData)) { + $this->handlePutRequest($inputData); + $this->getResponseJson(); + } else { + $errorResponse = ["success" => false, "error" => $objQloCMConnector->l($this->error_msg)]; + echo json_encode($errorResponse, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT); + $this->deleteWsTempObjects(); + exit; + } + } + } catch (\Throwable $e) { + if ($this->currentBookingId) { + try { + $this->getBookingDetails($this->currentBookingId); + } catch (\Throwable $inner) { + // getBookingDetails() itself failed too — CM still expects the "data" shape, + // so build the minimal version by hand rather than switching response format. + $this->output['data'] = ['id_pms_booking' => $this->currentBookingId]; + } + $this->output['data']['error'] = $objQloCMConnector->l($e->getMessage()); + $this->getResponseJson(); + // no exit here — same as the success path, $this->output is picked up + // and echoed by the webservice framework after manage() returns. + } else { + $errorResponse = ["success" => false, "error" => $objQloCMConnector->l($e->getMessage())]; + echo json_encode($errorResponse, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT); + $this->deleteWsTempObjects(); + exit; + } + } finally { + // exit skips finally, so each exit path above cleans up itself first. + $this->deleteWsTempObjects(); + } + break; + default: + $errorResponse = ["success" => false, "error" => $objQloCMConnector->l('This HTTP method is not allowed')]; + echo json_encode($errorResponse, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT); + exit; + } + } + + return $this->wsObject->getOutputEnabled(); + } + + public function validateFilters($filters = []) + { + $objQloCMConnector = Module::getInstanceByName('qlocmconnector'); + $allowedFilters = [ + 'id_property' => 'int', + 'id_room_type' => 'string', + 'date_updated' => ['gte', 'gt', 'lt', 'lte'], + 'check_out' => ['gte', 'gt', 'lt', 'lte'], + 'check_in' => ['gte', 'gt', 'lt', 'lte'] + ]; + + $validatedFilters = []; + + foreach ($filters as $key => $value) { + if (is_array($value)) { + if (!isset($allowedFilters[$key]) || !is_array($allowedFilters[$key])) { + $errorResponse = ["success" => false, "error" => $objQloCMConnector->l("Invalid filter: $key")]; + echo json_encode($errorResponse, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT); + exit; + } + + foreach ($value as $operator => $subValue) { + if (!in_array($operator, $allowedFilters[$key])) { + $errorResponse = ["success" => false, "error" => $objQloCMConnector->l("Invalid filter operator: $operator for $key")]; + echo json_encode($errorResponse, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT); + exit; + } + + if ($key === 'date_updated' && !$this->validateDate($subValue)) { + $errorResponse = ["success" => false, "error" => $objQloCMConnector->l("Invalid date format for $key[$operator]. Expected YYYY-MM-DD.")]; + echo json_encode($errorResponse, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT); + exit; + } + + $validatedFilters["filter[$key][$operator]"] = $subValue; + } + } else { + if (!isset($allowedFilters[$key]) || ($key == 'id_room_type' && $filters[$key] == '0')) { + $errorResponse = ["success" => false, "error" => $objQloCMConnector->l("Invalid filter: $key")]; + echo json_encode($errorResponse, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT); + exit; + } + + if ($key === 'id_property' && !filter_var($value, FILTER_VALIDATE_INT)) { + $errorResponse = ["success" => false, "error" => $objQloCMConnector->l("Invalid value for id_property. Must be an integer.")]; + echo json_encode($errorResponse, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT); + exit; + } + + $validatedFilters["filter[$key]"] = $value; + } + } + + return $validatedFilters; + } + + // Helper function to validate date format + public function validateDate($date, $format = 'Y-m-d H:i:s') + { + $d = DateTime::createFromFormat($format, $date); + return $d && $d->format($format) === $date; + } + + + /** + * Creating feature price for the room. + * Also they will be deleted after the booking has been created or updated. + */ + public function createFeaturePrice($params) + { + $numDays = HotelHelper::getNumberOfDays($params['date_from'], $params['date_to']); + if (!$numDays) { + $numDays = 1; + } + + if ($this->context->cart->id_currency != (int)Configuration::get('PS_CURRENCY_DEFAULT')) { + $currency = Currency::getCurrencyInstance($this->context->cart->id_currency); + $params['price'] = Tools::ps_round($params['price']/$currency->conversion_rate, 6); + } + + $params['name'] = 'Api-Booking-Price'; + $params['impact_type'] = HotelRoomTypeFeaturePricing::IMPACT_TYPE_FIXED_PRICE; + // Stored precision (6 dp), so the nights add back up to the sent total. + $params['impact_value'] = Tools::ps_round($params['price']/$numDays, 6); + $params['is_special_days_exists'] = 0; + $params['special_days'] = json_encode(false); + $params['restrictions'] = array( + array( + 'date_from' => $params['date_from'], + 'date_to' => $params['date_to'] + ) + ); + + return HotelRoomTypeFeaturePricing::createRoomTypeFeaturePrice($params); + } + + /** + * Get the JSON response. + */ + public function getResponseJson() + { + if ($this->output && is_array($this->output)) { + $this->output = json_encode($this->output); + } + } + + public function getContent() + { + + $content = $this->output; + if(!$content) { + $content = json_encode([]); + } + $array = json_decode($content, true); + + if (!is_array($array)) { + $outputXML = simplexml_load_string($content, null, LIBXML_NOCDATA); + $content = json_encode($outputXML); + } + + $this->objOutput->setHeaderParams('Content-Type', 'application/json'); + $content = preg_replace_callback( + '/\\\\u([a-f0-9]{4})/', + function ($matches) { + return iconv( + 'UCS-4LE', + 'UTF-8', + pack( + 'V', + hexdec($matches[1]) + ) + ); + }, + $content + ); + + header('Content-Type: application/json'); + + return $content; + } + + /** + * Validating the POST request. + */ + public function validatePostRequest($params) + { + $this->error_msg = ''; + if (isset($params['id'])) { + $this->error_msg = Tools::displayError('id is forbidden when adding a new resource'); + } elseif (!isset($params['currency']) + || !$params['currency'] + || !Currency::getIdByIsoCode($params['currency']) + || (!Validate::isLoadedObject((new Currency(Currency::getIdByIsoCode($params['currency']))))) + ) { + $this->error_msg = Tools::displayError('Please provide valid currency.'); + } elseif (!isset($params['guest_detail']) + || !$params['guest_detail'] + ) { + $this->error_msg = Tools::displayError('Customer details not found.'); + } elseif (!isset($params['id_property']) + || !Validate::isLoadedObject(new HotelBranchInformation((int) $params['id_property'])) + ) { + $this->error_msg = Tools::displayError('Please provide a valid id_property.'); + } elseif (isset($params['guest_detail']['id_customer']) + && $params['guest_detail']['id_customer'] + && !Validate::isLoadedObject(new Customer((int) $params['guest_detail']['id_customer'])) + ) { + $this->error_msg = Tools::displayError('Please provide a valid id_customer.'); + } elseif (!isset($params['guest_detail']['firstname']) + || !$params['guest_detail']['firstname'] + || empty(trim($params['guest_detail']['firstname'])) + ) { + $this->error_msg = Tools::displayError('Please provide a valid first name.'); + } elseif (!isset($params['guest_detail']['lastname']) + || !$params['guest_detail']['lastname'] + || empty(trim($params['guest_detail']['lastname'])) + ) { + $this->error_msg = Tools::displayError('Please provide a valid last name.'); + } elseif (!isset($params['guest_detail']['email']) + || !$params['guest_detail']['email'] + || empty(trim($params['guest_detail']['email'])) + ) { + $this->error_msg = Tools::displayError('Please provide a valid email.'); + } elseif (Configuration::get('PS_ONE_PHONE_AT_LEAST') + && (!isset($params['guest_detail']['phone']) || !$params['guest_detail']['phone'] || empty(trim($params['guest_detail']['phone']))) + ) { + $this->error_msg = Tools::displayError('Please provide a valid phone number.'); + } elseif (isset($params['price_details']['total_price_with_tax']) + && $params['price_details']['total_price_with_tax'] + && (!Validate::isPrice($params['price_details']['total_price_with_tax'])) + ) { + $this->error_msg = Tools::displayError('Invalid value for total paid.'); + } elseif (isset($params['price_details']['total_price_with_tax']) + && $params['price_details']['total_price_with_tax'] + && (!Validate::isPrice($params['price_details']['total_price_with_tax'])) + ) { + $this->error_msg = Tools::displayError('Invalid value for total amount with tax.'); + } elseif (!$this->validateAddressFields($params['guest_detail']) + && $this->error_msg == '' + ) { + $this->error_msg = Tools::displayError('Invalid address provided.'); + } elseif (!$this->validateRequestedRoomTypes($params['room_bookings'], $params['id_property']) + && $this->error_msg == '' + ) { + $this->error_msg = Tools::displayError('Requested room(s) not available.'); + } elseif (isset($params['payment_detail']['payment_type']) + && $params['payment_detail']['payment_type'] != 'online' + && $params['payment_detail']['payment_type'] != 'remote' + && $params['payment_detail']['payment_type'] != 'pay at hotel' + ) { + $this->error_msg = Tools::displayError('Invalid payment type.'); + } elseif (!isset($params['booking_status'])) { + $this->error_msg = Tools::displayError('Invalid booking status.'); + } + + if (!$this->error_msg && $this->error_msg == '') { + return true; + } + + return false; + } + + /** + * Validating the address fields in POST and PUT requests. + */ + public function validateAddressFields($params) + { + $status = true; + if (isset($params['address']) + || isset($params['city']) + || isset($params['country_code']) + || isset($params['state_code']) + || isset($params['zip']) + ) { + $status = false; + $idCountry = Country::getByIso($params['country_code']); + $objCountry = new Country($idCountry); + if (!isset($params['address']) || !$params['address']) { + $this->error_msg = Tools::displayError('Address is required.'); + } elseif (!isset($params['city']) || !$params['city']) { + $this->error_msg = Tools::displayError('City is required.'); + } elseif (!isset($params['country_code']) || !$params['country_code']) { + $this->error_msg = Tools::displayError('Country code is required.'); + } else if (!isset($params['address']) || !$params['address']) { + $this->error_msg = Tools::displayError('Address is required.'); + } else if (!isset($params['country_code']) || !$params['country_code']) { + $this->error_msg = Tools::displayError('Country code is required.'); + } else if (!$idCountry = Country::getByIso($params['country_code'])) { + $this->error_msg = Tools::displayError('Invalid country code.'); + } else if ($objCountry->need_zip_code && (!isset($params['zip']) || !$params['zip'])) { + $this->error_msg = Tools::displayError('Zip code is required.'); + } elseif ($objCountry->need_zip_code + && (!Validate::isPostCode($params['zip']) || ($objCountry->zip_code_format && !$objCountry->checkZipCode($params['zip']))) + ) { + $this->error_msg = sprintf(Tools::displayError('The Zip/Postal code you have entered is invalid. It must follow this format: %s'), str_replace('C', $objCountry->iso_code, str_replace('N', '0', str_replace('L', 'A', $objCountry->zip_code_format)))); + } else { + $status = true; + } + } + + return $status; + } + + + /** + * Requested occupancy for the partial availability search: max guests when booked by + * quantity, else adults/children/child ages per room (missing ages default to 17). + * + * @param array $roomType requested room type + * @param bool $quantityWise + * @param array|false $hotelRoomData result of HotelBookingDetail::dataForFrontSearch() + * + * @return int|array + */ + protected function buildRequestedOccupancy($roomType, $quantityWise, $hotelRoomData) + { + if ($quantityWise) { + return isset($hotelRoomData['rm_data'][$roomType['id_room_type']]['max_guests']) + ? $hotelRoomData['rm_data'][$roomType['id_room_type']]['max_guests'] + : 0; + } + + $occupancy = array(); + if (isset($roomType['rooms']) && count($roomType['rooms'])) { + foreach ($roomType['rooms'] as $room) { + $child_ages = isset($room['occupancy']['children_ages']) ? $room['occupancy']['children_ages'] : (isset($room['occupancy']['child_ages']) ? $room['occupancy']['child_ages'] : array()); + + // default age for missing ones + $child_ages = array_pad($child_ages, isset($room['occupancy']['children']) ? $room['occupancy']['children'] : 0, 17); + + $occupancy[] = array( + 'adults' => isset($room['occupancy']['adults']) ? $room['occupancy']['adults'] : 0, + 'children' => isset($room['occupancy']['children']) ? $room['occupancy']['children'] : 0, + 'child_ages' => $child_ages + ); + } + } + + return $occupancy; + } + + /** + * Checking room type information validity. + */ + public function validateRequestedRoomTypes($roomTypes = array(), $idHotel = false, $idOrder = false) + { + if (!$roomTypes) { + return false; + } + + $objBookingDetail = new HotelBookingDetail(); + $objRoomType = new HotelRoomType(); + $roomUnitSelectionType = Configuration::get('PS_FRONT_ROOM_UNIT_SELECTION_TYPE'); + $nonBookableRooms = array(); + + $quantityWise = false; + if ($roomUnitSelectionType != HotelBookingDetail::PS_ROOM_UNIT_SELECTION_TYPE_OCCUPANCY) { + $quantityWise = true; + } + + foreach ($roomTypes as $roomType) { + if ($this->validateRoomType($roomType)) { + $roomTypeInfo = $objRoomType->getRoomTypeInfoByIdProduct($roomType['id_room_type']); + if (!$idHotel) { + $idHotel = $roomTypeInfo['id_hotel']; + } elseif ($idHotel != $roomTypeInfo['id_hotel']) { + $this->error_msg = sprintf( + Tools::displayError('Room type %d does not belong to property %d.'), + $roomType['id_room_type'], + $idHotel + ); + return false; + } + + + $dateFrom = date('Y-m-d', strtotime($roomType['check_in_date'])); + $dateTo = date('Y-m-d', strtotime($roomType['check_out_date'])); + $bookingParams = array( + 'date_from' => $dateFrom, + 'date_to' => $dateTo, + 'hotel_id' => $idHotel, + 'num_rooms' => $roomType['number_of_rooms'], + 'id_room_type' => $roomType['id_room_type'], + 'only_search_data' => 1, + 'id_order' => $idOrder + ); + + if (($hotelRoomData = $objBookingDetail->dataForFrontSearch($bookingParams)) + && isset($hotelRoomData['rm_data'][$roomType['id_room_type']]['data']['available']) + && $hotelRoomData['rm_data'][$roomType['id_room_type']]['data']['available'] + ) { + + if ($hotelRoomData['stats']['num_avail'] < $roomType['number_of_rooms']) { + if(!$this->checkForPariallyAvailableRooms(array( + 'date_from' => $dateFrom, + 'date_to' => $dateTo, + 'hotel_id' => $idHotel, + 'id_room_type' => $roomType['id_room_type'], + 'number_of_rooms' => $roomType['number_of_rooms'], + 'occupancy' => $this->buildRequestedOccupancy($roomType, $quantityWise, $hotelRoomData) + ), $this->context->cart->id, $idOrder)) { + return false; + } + } elseif (isset($roomType['rooms']) && count($roomType['rooms'])) { + foreach ($roomType['rooms'] as $room) { + if (isset($room['id_room'])) { + // Validate that the room belongs to the correct hotel + $objRoom = new HotelRoomInformation($room['id_room']); + if (Validate::isLoadedObject($objRoom) && $objRoom->id_hotel != $idHotel) { + $this->error_msg = Tools::displayError('Room does not belong to the specified hotel.'); + return false; + } + if (!isset($hotelRoomData['rm_data'][$roomType['id_room_type']]['data']['available'][$room['id_room']])) { + return false; + } + } + } + } + } else { + $occupancy = $this->buildRequestedOccupancy($roomType, $quantityWise, $hotelRoomData); + + if(!($partiallyAvailableRooms = $this->checkForPariallyAvailableRooms(array( + 'date_from' => $dateFrom, + 'date_to' => $dateTo, + 'hotel_id' => $idHotel, + 'id_room_type' => $roomType['id_room_type'], + 'search_available' => 1, + 'number_of_rooms' => $roomType['number_of_rooms'], + 'occupancy' => $occupancy + ), $this->context->cart->id, $idOrder))) { + return false; + } + } + } else { + return false; + } + } + + return true; + } + + /** + * Validating the Room types fields. + */ + public function validateRoomType($roomType) + { + $status = true; + if (!isset($roomType['id_room_type'])) { + $this->error_msg = Tools::displayError('Id room type is missing'); + $status = false; + } elseif (!isset($roomType['check_in_date']) || !Validate::isDate($roomType['check_in_date'])) { + $this->error_msg = Tools::displayError('Invalid check in date in the request'); + $status = false; + } elseif (!isset($roomType['check_out_date']) || !Validate::isDate($roomType['check_out_date'])) { + $this->error_msg = Tools::displayError('Invalid check out date in the request'); + $status = false; + } elseif (!isset($roomType['number_of_rooms'])) { + $this->error_msg = Tools::displayError('number of rooms is missing'); + $status = false; + } elseif (!Validate::isLoadedObject(new Product((int) $roomType['id_room_type'])) + || !Product::isBookingProduct((int) $roomType['id_room_type']) + ) { + $this->error_msg = Tools::displayError('Invalid room type in the request'); + $status = false; + } elseif (isset($roomType['rooms']) && count($roomType['rooms']) && count($roomType['rooms']) != $roomType['number_of_rooms']) { + $this->error_msg = Tools::displayError('Room count does not matches with the number of rooms'); + $status = false; + } elseif (strtotime($roomType['check_in_date']) > strtotime($roomType['check_out_date'])) { + $this->error_msg = Tools::displayError('Invalid check in and check out dates in the request'); + $status = false; + } + + return $status; + } + + /** + * Operations required for PUT requests. + */ + public function handlePostRequest($params) + { + QloCMConnector::$idBookingCm = isset($params['id_channel_manager_booking']) && $params['id_channel_manager_booking'] + ? $params['id_channel_manager_booking'] + : uniqid('CM_API_', true); + + $this->context->cart = new Cart(); + $this->processGuestDetails($params['guest_detail']); + $this->processLanguage($params); + $this->processCurrency($params); + // Saving the cart after adding the guest, language and the currency in the cart. + $this->context->cart->save(); + $this->processCustomer($params['guest_detail']); + if (!$this->addRoomsInCart($params['room_bookings'])) { + QloCMConnector::$idBookingCm = null; + $this->deleteWsTempObjects(); + throw new WebserviceException( + ($this->error_msg ?: Tools::displayError('Requested room(s) not available.')), + array(404, 400) + ); + } + // validating Cart rules here since the cart rule checkValidity() only works if there are products in the cart. + if (($error = $this->applyCartRules($params)) && $error != '') { + QloCMConnector::$idBookingCm = null; + throw new WebserviceException( + $error, + array(404, 400) + ); + + return false; + } + + $totalAmount = isset($params['price_details']['total_price_with_tax']) ? $params['price_details']['total_price_with_tax'] : 0; + $totalTax = isset($params['price_details']['total_tax']) ? $params['price_details']['total_tax'] : 0; + $objPaymentModule = new WebserviceOrder(); + $bookingStatus = self::API_BOOKING_STATUS_NEW; + if (isset($params['booking_status']) && $params['booking_status']) { + $bookingStatus = $params['booking_status']; + } + + $cartTotal = $this->context->cart->getOrderTotal(false, Cart::BOTH); + + switch ($bookingStatus) { + case self::API_BOOKING_STATUS_NEW: + $paymentStatus = false; + if (isset($params['payment_status'])) { + $paymentStatus = $params['payment_status']; + } + + switch ($paymentStatus) { + case self::API_BOOKING_PAYMENT_STATUS_COMPLETED: + $orderStatus = Configuration::get('PS_OS_PAYMENT_ACCEPTED'); + break; + case self::API_BOOKING_PAYMENT_STATUS_PARTIAL: + $sentPaidAmount = $this->getSentPaidAmount($params); + if ($sentPaidAmount !== null && $totalAmount > 0 && $sentPaidAmount >= $totalAmount) { + $orderStatus = Configuration::get('PS_OS_PAYMENT_ACCEPTED'); + } else { + $orderStatus = Configuration::get('PS_OS_PARTIAL_PAYMENT_ACCEPTED'); + } + break; + case self::API_BOOKING_PAYMENT_STATUS_AWATING: + $orderStatus = Configuration::get('PS_OS_AWAITING_PAYMENT'); + break; + default: + $orderStatus = Configuration::get('PS_OS_AWAITING_PAYMENT'); + break; + } + break; + case self::API_BOOKING_STATUS_COMPLETED: + $orderStatus = Configuration::get('PS_OS_PAYMENT_ACCEPTED'); + if (!$totalAmount) { + $totalAmount = $cartTotal; + } elseif ($totalAmount > 0 && $totalAmount < $cartTotal) { + $orderStatus = Configuration::get('PS_OS_PARTIAL_PAYMENT_ACCEPTED'); + } + + break; + case self::API_BOOKING_STATUS_CANCELLED: + $orderStatus = Configuration::get('PS_OS_CANCELED'); + break; + case self::API_BOOKING_STATUS_REFUNDED: + $orderStatus = Configuration::get('PS_OS_REFUND'); + break; + default: + break; + } + + // Kept before the paid/unpaid collapse below, to record a partial payment later. + $originalOrderStatus = $orderStatus; + + if (isset($params['source']) && $params['source']) { + $objPaymentModule->orderSource = $params['source']; + } + + $extraVars = array(); + $message = null; + if (isset($params['payment_detail']['transaction_id']) + && $params['payment_detail']['transaction_id'] + ) { + $extraVars['transaction_id'] = $params['payment_detail']['transaction_id']; + } + + if(isset($params['payment_status']) && $params['payment_status'] == self::API_BOOKING_PAYMENT_STATUS_COMPLETED) { + $objPaymentModule->payment_type = OrderPayment::PAYMENT_TYPE_REMOTE_PAYMENT; + } + + if (isset($params['remark'])) { + $message = $params['remark']; + } + + if (isset($params['payment_detail']['payment_method']) + && $params['payment_detail']['payment_method'] + ) { + $objPaymentModule->displayName = $params['payment_detail']['payment_method']; + } + + if (isset($params['payment_detail']['payment_type']) + && $params['payment_detail']['payment_type'] + ) { + if ($params['payment_detail']['payment_type'] == 'remote') { + $objPaymentModule->payment_type = OrderPayment::PAYMENT_TYPE_REMOTE_PAYMENT; + } elseif ($params['payment_detail']['payment_type'] == 'pay at hotel') { + $objPaymentModule->payment_type = OrderPayment::PAYMENT_TYPE_PAY_AT_HOTEL; + } + } + $objOrderState = new OrderState($orderStatus); + if ($objOrderState->paid) { + $orderStatus = Configuration::get('PS_OS_PAYMENT_ACCEPTED'); + } else { + $orderStatus = Configuration::get('PS_OS_AWAITING_PAYMENT'); + } + + if ($objPaymentModule->validateOrder( + $this->context->cart->id, + $orderStatus, + $totalAmount, + $objPaymentModule->displayName, + $message, + $extraVars, + null, + false, + $this->bookingCustomer->secure_key, + null, + false + )) { + $this->currentBookingId = (int) $objPaymentModule->currentOrder; + $this->updateServicesInOrder($objPaymentModule->currentOrder); + if (!empty($this->wsRequestedRooms)) { + $this->updateRoomTaxRulesGroupsInOrder($objPaymentModule->currentOrder); + } + $this->manageOrderPrice($objPaymentModule->currentOrder, $params); + $this->recalculateOrderFromComponents($objPaymentModule->currentOrder); + $this->addOrderHistory($objPaymentModule->currentOrder, $objOrderState); + $objOrder = new Order($objPaymentModule->currentOrder); + if (isset($params['booking_date']) + && $params['booking_date'] + && Validate::isDate($params['booking_date']) + ) { + $objOrder->date_add = date('Y-m-d H:i:s', strtotime($params['booking_date'])); + } + + if(isset($params['id_channel_manager_booking']) && $params['id_channel_manager_booking']) { + $objOrder->id_channel_manager_booking = $params['id_channel_manager_booking']; + } + + if (QcmcSourceHelper::coreClassesExist() + && isset($params['source']) + && $params['source'] + ) { + // The CM never sends business_source; default to OTA. + $businessSource = (isset($params['business_source']) && $params['business_source']) + ? $params['business_source'] + : 'OTA'; + $objSourceHelper = new QcmcSourceHelper(); + $objOrder->id_source = $objSourceHelper->getBookingSourceId($params['source'], $businessSource); + } + + // Partial payment: record the received amount if the CM sent one (see getSentPaidAmount()). + if (isset($paymentStatus) + && $paymentStatus == self::API_BOOKING_PAYMENT_STATUS_PARTIAL + && $originalOrderStatus == Configuration::get('PS_OS_PARTIAL_PAYMENT_ACCEPTED') + && ($receivedAmount = $this->getSentPaidAmount($params)) + ) { + // Linked to the order invoice, like core payments. + $objOrder->addOrderPayment( + $receivedAmount, + null, + isset($extraVars['transaction_id']) ? $extraVars['transaction_id'] : null, + null, + null, + $objOrder->getInvoicesCollection()->getFirst() ?: null, + $objPaymentModule->payment_type == OrderPayment::PAYMENT_TYPE_PAY_AT_HOTEL + ? OrderPayment::PAYMENT_TYPE_PAY_AT_HOTEL + : OrderPayment::PAYMENT_TYPE_REMOTE_PAYMENT + ); + + } + + $objOrder->advance_paid_amount = $totalAmount; + if ($params['payment_status'] == self::API_BOOKING_PAYMENT_STATUS_COMPLETED + || $params['payment_status'] == self::API_BOOKING_PAYMENT_STATUS_AWATING + ) { + $objOrder->is_advance_payment = 0; + } else { + $objOrder->advance_paid_amount = $objOrder->total_paid_real; + } + $objOrder->save(); + + $this->getBookingDetails($objPaymentModule->currentOrder); + QloCMConnector::$idBookingCm = null; + + return true; + } + + QloCMConnector::$idBookingCm = null; + $this->wsObject->setError(400, Tools::displayError('Unable to create booking.'), 200); + + return false; + } + + /** + * Used to get the booking details for the GET, POST and PUT request. + */ + public function getBookingDetails($idBooking) + { + $objOrder = new Order($idBooking); + if (!Validate::isLoadedObject($objOrder)) { + $this->objOutput->setStatus(404); + $this->getWsObject()->setOutputEnabled(false); + return false; + } else { + $objCurrency = new Currency($objOrder->id_currency); + $objBookingDetail = new HotelBookingDetail(); + $objServiceProductOrderDetail = new ServiceProductOrderDetail(); + $objOrderReturn = new OrderReturn(); + $idHotel = HotelBookingDetail::getIdHotelByIdOrder($objOrder->id); + $objHotelBranchInformation = new HotelBranchInformation($idHotel, Configuration::get('PS_LANG_DEFAULT')); + $objOrderState = new OrderState($objOrder->current_state, Configuration::get('PS_LANG_DEFAULT')); + $objCustomer = new Customer($objOrder->id_customer); + $params['id'] = (int) $objOrder->id; + $params['id_pms_booking'] = (int) $objOrder->id; + $params['id_property'] = (int) $idHotel; + if(isset($objOrder->id_channel_manager_booking) && $objOrder->id_channel_manager_booking) { + $params['id_channel_manager_booking'] = $objOrder->id_channel_manager_booking; + } + $params['currency'] = strtoupper($objCurrency->iso_code); + // $params['current_status'] = $objOrderState->name; + $params['source'] = $objOrder->source; + $params['booking_date'] = $objOrder->date_add; + $params['id_language'] = (int) $objOrder->id_lang; + $assoc = array(); + $customerDetails = array( + 'id_customer' => (int) $objCustomer->id, + 'firstname' => $objCustomer->firstname, + 'lastname' => $objCustomer->lastname, + 'email' => $objCustomer->email, + 'phone' => isset($objCustomer->phone) ? $objCustomer->phone : '' + ); + + $priceDetails = array( + 'total_paid' => Tools::ps_round($objOrder->total_paid_real, _PS_PRICE_COMPUTE_PRECISION_), + 'total_price_without_tax' => Tools::ps_round($objOrder->total_paid_tax_excl, _PS_PRICE_COMPUTE_PRECISION_), + 'total_tax' => Tools::ps_round(($objOrder->total_paid_tax_incl - $objOrder->total_paid_tax_excl), _PS_PRICE_COMPUTE_PRECISION_) + ); + + $orderCartRules = array(); + if ($cartRules = $objOrder->getCartRules()) { + foreach ($cartRules as $cartRule) { + $rule = array(); + $rule['code'] = $cartRule['name']; + $rule['value'] = Tools::ps_round($cartRule['value'], _PS_PRICE_COMPUTE_PRECISION_); + $rule['currency'] = strtoupper($objCurrency->iso_code); + $rule['id_order_invoice'] = (int) $cartRule['id_order_invoice']; + $orderCartRules[] = $rule; + } + } + + $params['associations']['guest_detail'] = $customerDetails; + $params['associations']['price_details'] = $priceDetails; + $params['associations']['cart_rules'] = $orderCartRules; + $roomTypeInfo = array(); + if (Group::getPriceDisplayMethod($objCustomer->id_default_group) == PS_TAX_INC) { + $useTax = 1; + } else { + $useTax = null; + } + + $params['associations']['remarks'] = array(); + if ($customerMessages = Message::getMessagesByOrderId($objOrder->id, true)) { + foreach ($customerMessages as $customerMessage) { + $message = $customerMessage['message']; + $params['associations']['remarks'][] = $message; + } + } + + + if ($orderDetailData = $objBookingDetail->getOrderFormatedBookinInfoByIdOrder($objOrder->id)) { + foreach ($orderDetailData as $orderDetailKey => $orderData) { + $dateJoin = $orderData['id_product'].'_'.strtotime($orderData['date_from']).strtotime($orderData['date_to']); + if (!isset($roomTypeInfo[$dateJoin])) { + $roomTypeInfo[$dateJoin]['id_room_type'] = (int) $orderData['id_product']; + $roomTypeInfo[$dateJoin]['checkin_date'] = $orderData['date_from']; + $roomTypeInfo[$dateJoin]['checkout_date'] = $orderData['date_to']; + $roomTypeInfo[$dateJoin]['total_tax'] = ($orderData['total_price_tax_incl'] - $orderData['total_price_tax_excl']); + $roomTypeInfo[$dateJoin]['number_of_rooms'] = 1; + $roomTypeInfo[$dateJoin]['name'] = $orderData['room_type_name']; + } else { + $roomTypeInfo[$dateJoin]['total_tax'] += ($orderData['total_price_tax_incl'] - $orderData['total_price_tax_excl']); + $roomTypeInfo[$dateJoin]['number_of_rooms'] += 1; + } + + $roomTypeInfo[$dateJoin]['total_tax'] = Tools::ps_round($roomTypeInfo[$dateJoin]['total_tax'], _PS_PRICE_COMPUTE_PRECISION_); + + $roomInfo = array(); + $roomInfo['id_room'] = (int) $orderData['id_room']; + $roomInfo['id_hotel_booking'] = (int) $orderData['id']; + $roomInfo['adults'] = (int) $orderData['adults']; + $roomInfo['child'] = (int) $orderData['children']; + $roomInfo['unit_price_without_tax'] = Tools::ps_round($orderData['total_price_tax_excl'], _PS_PRICE_COMPUTE_PRECISION_); + $roomInfo['total_tax'] = Tools::ps_round(($orderData['total_price_tax_incl'] - $orderData['total_price_tax_excl']), _PS_PRICE_COMPUTE_PRECISION_); + if (isset($roomInfo['services'])) { + unset($roomInfo['services']); + } + + if ($additionalServices = $objServiceProductOrderDetail->getroomTypeServiceProducts( + $orderData['id_order'], + 0, + 0, + $orderData['id_product'], + $orderData['date_from'], + $orderData['date_to'], + $orderData['id_room'], + 0, + $useTax + )) { + $roomInfo['services'] = array(); + foreach ($additionalServices as $additionalService) { + foreach ($additionalService['additional_services'] as $service) { + $services = array(); + $services['id_service'] = (int) $service['id_product']; + $services['name'] = $service['name']; + $services['quantity'] = (int) $service['quantity']; + $services['unit_price_without_tax'] = Tools::ps_round(($service['total_price_tax_excl'] / $services['quantity']), _PS_PRICE_COMPUTE_PRECISION_); + $services['total_price_without_tax'] = Tools::ps_round(($service['total_price_tax_excl']), _PS_PRICE_COMPUTE_PRECISION_); + $services['total_tax'] = Tools::ps_round(($service['total_price_tax_incl'] - $service['total_price_tax_excl']), _PS_PRICE_COMPUTE_PRECISION_); + + $objProduct = new Product($service['id_product']); + $services['per_night'] = 0; + if (self::isPerNightPriceCalculationMethod($objProduct->price_calculation_method)) { + $services['per_night'] = 1; + } + + $services['price_mode'] = $services['per_night'] ? self::API_SERVICE_PRICE_MODE_PER_NIGHT : self::API_SERVICE_PRICE_MODE_PER_BOOKING; + $roomInfo['services'][] = $services; + } + } + } + + $roomTypeInfo[$dateJoin]['rooms'][] = $roomInfo; + } + } + + $params['associations']['room_types'] = array_values($roomTypeInfo); + $this->output['data'] = $params; + } + } + + /** + * Deletes the request's temporary service products, feature prices and cart rules. + * Safe to call more than once: the lists are emptied after deletion. + * + * @return void + */ + protected function deleteWsTempObjects() + { + $this->deleteWsServices(); + $this->deleteWsFeaturePrices(); + $this->deleteWsCartRules(); + $this->wsIdServices = array(); + $this->wsFeaturePrices = array(); + $this->wsCartRules = array(); + } + + /** + * Deleting the newly created services. + */ + public function deleteWsServices() + { + if (isset($this->wsIdServices) && $this->wsIdServices) { + foreach ($this->wsIdServices as $wsIdService) { + // To filter false ids + if ((int) $wsIdService) { + $objProduct = new Product($wsIdService); + $objProduct->delete(); + } + } + } + } + + /** + * Deleting the newly created feature price. + */ + public function deleteWsFeaturePrices() + { + if (isset($this->wsFeaturePrices) && $this->wsFeaturePrices) { + foreach ($this->wsFeaturePrices as $idFeaturePrice) { + // To filter false ids + if ((int) $idFeaturePrice && Validate::isLoadedObject($objFeaturePrice = new HotelRoomTypeFeaturePricing((int) $idFeaturePrice))) { + $objFeaturePrice->delete(); + } + } + } + } + + /** + * Deleting the newly created cart Rules. + */ + public function deleteWsCartRules() + { + if (isset($this->wsCartRules) && $this->wsCartRules) { + foreach ($this->wsCartRules as $idCartRule) { + // Keep cart rules applied to the order (order_cart_rule references them). + $objCartRule = new CartRule((int) $idCartRule); + if (!Validate::isLoadedobject($objCartRule)) { + continue; + } + + if (Db::getInstance()->getValue( + 'SELECT id_order_cart_rule FROM `'._DB_PREFIX_.'order_cart_rule` WHERE id_cart_rule='.(int) $idCartRule + )) { + // Applied directly to the order, so core never used it up: disable it to block reuse. + $objCartRule->quantity = 0; + $objCartRule->highlight = 0; + $objCartRule->save(); + continue; + } + + $objCartRule->delete(); + } + } + } + + public function updateRoomTaxRulesGroupsInOrder($idOrder) + { + $objOrder = new Order($idOrder); + $objHotelBookingDetail = new HotelBookingDetail(); + if ($roomsInOrder = $objHotelBookingDetail->getOrderCurrentDataByOrderId($idOrder)) { + foreach ($roomsInOrder as $orderRoomKey => $orderRoom) { + $dateRoomJoinKey = strtotime($orderRoom['date_from']).''.strtotime($orderRoom['date_to']).$orderRoom['id_product'].$orderRoom['id_room']; + if (isset($this->wsRequestedRooms[$dateRoomJoinKey])) { + $objHotelBookingDetail = new HotelBookingDetail((int) $orderRoom['id']); + $objOrderDetail = new OrderDetail((int) $objHotelBookingDetail->id_order_detail); + + $objAddress = new Address((int) $objOrder->id_address_tax); + $priceWithTax = $objHotelBookingDetail->total_price_tax_incl; + + if (isset($this->wsRequestedRooms[$dateRoomJoinKey]['id_tax_rules_group'])) { + // Getting new Tax + $objOrderDetail->id_tax_rules_group = $this->wsRequestedRooms[$dateRoomJoinKey]['id_tax_rules_group']; + if (isset($this->wsRequestedRooms[$dateRoomJoinKey]['total_tax'])) { + // Use the sent total_tax, not a recomputed rate; the group is kept for tax rows. + $priceWithTax = $objHotelBookingDetail->total_price_tax_excl + $this->wsRequestedRooms[$dateRoomJoinKey]['total_tax']; + } else { + $objTaxManager = TaxManagerFactory::getManager($objAddress, $this->wsRequestedRooms[$dateRoomJoinKey]['id_tax_rules_group']); + $objTaxCalculator = $objTaxManager->getTaxCalculator(); + $priceWithTax = $objTaxCalculator->addTaxes($objHotelBookingDetail->total_price_tax_excl); + } + } else if (isset($this->wsRequestedRooms[$dateRoomJoinKey]['total_tax'])) { + $priceWithTax = $objHotelBookingDetail->total_price_tax_excl + $this->wsRequestedRooms[$dateRoomJoinKey]['total_tax']; + $objOrderDetail->id_tax_rules_group = 0; + Db::getInstance()->execute('DELETE FROM `'._DB_PREFIX_.'order_tax_detail` WHERE id_order_detail='.(int)$objOrderDetail->id.' AND id_htl_booking IN (0, '.(int)$objHotelBookingDetail->id.')'); + } + + $taxDiff = $priceWithTax - $objHotelBookingDetail->total_price_tax_incl; + + // Updating the price + $objHotelBookingDetail->total_price_tax_incl += $taxDiff; + $objHotelBookingDetail->total_paid_amount = $objHotelBookingDetail->total_price_tax_incl; + $objHotelBookingDetail->save(); + + $objOrderDetail->total_price_tax_incl += $taxDiff; + $objOrderDetail->unit_price_tax_incl = Tools::ps_round(($objOrderDetail->total_price_tax_incl / $objOrderDetail->product_quantity), _PS_PRICE_COMPUTE_PRECISION_); + $objOrderDetail->unit_price_tax_excl = Tools::ps_round(($objOrderDetail->total_price_tax_excl / $objOrderDetail->product_quantity), _PS_PRICE_COMPUTE_PRECISION_); + if (($objOrderDetail->id_order_invoice) + && Validate::isLoadedObject($objOrderInvoice = new OrderInvoice($objOrderDetail->id_order_invoice)) + ) { + $objOrderInvoice->total_products_wt += $taxDiff; + $objOrderInvoice->total_paid_tax_incl += $taxDiff; + $objOrderInvoice->save(); + } + + if (isset($this->wsRequestedRooms[$dateRoomJoinKey]['id_tax_rules_group']) && $this->wsRequestedRooms[$dateRoomJoinKey]['id_tax_rules_group']) { + $objOrderDetail->id_tax_rules_group = $this->wsRequestedRooms[$dateRoomJoinKey]['id_tax_rules_group']; + $objOrderDetail->save(); + // Rows scoped to this room; replaces its old rows and core's unscoped ones. + $this->saveTaxCalculator($objOrderDetail->id, $this->wsRequestedRooms[$dateRoomJoinKey]['id_tax_rules_group'], $objHotelBookingDetail->id); + } else { + $objOrderDetail->save(); + } + + $objOrder->total_paid += $taxDiff; + $objOrder->total_paid_tax_incl += $taxDiff; + if ($objOrder->current_state == Configuration::get('PS_OS_PAYMENT_ACCEPTED')) { + $objOrder->total_paid_real = Tools::ps_round($objOrder->total_paid_tax_incl, _PS_PRICE_COMPUTE_PRECISION_); + } + + $objOrder->total_products_wt += $taxDiff; + } + } + + $objOrder->update(); + } + } + + /** + * Writes order_tax_detail rows for one of the module's hidden tax rules groups. + * + * Scoped to a room or service line like OrderTaxDetail::updateVatScoping(), with the line's + * stored tax (incl - excl) split across the group's taxes by rate. Unscoped rows use the + * order_detail totals. + * + * @param int $idOrderDetail + * @param int $idTaxRulesGroup + * @param int $idHtlBooking pass at most one of $idHtlBooking/$idServiceProductOrderDetail + * @param int $idServiceProductOrderDetail + * + * @return bool + */ + public function saveTaxCalculator($idOrderDetail, $idTaxRulesGroup, $idHtlBooking = 0, $idServiceProductOrderDetail = 0) + { + $idOrderDetail = (int) $idOrderDetail; + $idHtlBooking = (int) $idHtlBooking; + $idServiceProductOrderDetail = (int) $idServiceProductOrderDetail; + + // A scoped write also drops core's unscoped (0/0) default-tax rows. + Db::getInstance()->execute( + 'DELETE FROM `' . _DB_PREFIX_ . 'order_tax_detail` + WHERE id_order_detail = ' . $idOrderDetail . ' + AND ((id_htl_booking = ' . $idHtlBooking . ' AND id_service_product_order_detail = ' . $idServiceProductOrderDetail . ')' + . (($idHtlBooking || $idServiceProductOrderDetail) ? ' OR (id_htl_booking = 0 AND id_service_product_order_detail = 0)' : '') . ')' + ); + + if (empty($this->wsTaxRulesGroup[$idTaxRulesGroup])) { + return false; + } + + $objOrderDetail = new OrderDetail($idOrderDetail); + $scopeTaxAmount = null; + if ($idHtlBooking) { + $objHotelBookingDetail = new HotelBookingDetail($idHtlBooking); + $scopeTaxAmount = $objHotelBookingDetail->total_price_tax_incl - $objHotelBookingDetail->total_price_tax_excl; + } elseif ($idServiceProductOrderDetail) { + $objServiceProductOrderDetail = new ServiceProductOrderDetail($idServiceProductOrderDetail); + $scopeTaxAmount = $objServiceProductOrderDetail->total_price_tax_incl - $objServiceProductOrderDetail->total_price_tax_excl; + } + + $totalRate = 0; + foreach ($this->wsTaxRulesGroup[$idTaxRulesGroup] as $taxRuleGroup) { + $totalRate += (float) $taxRuleGroup['tax_rate']; + } + + $values = array(); + foreach ($this->wsTaxRulesGroup[$idTaxRulesGroup] as $taxRuleGroup) { + $idTax = (int) $taxRuleGroup['tax']; + if (!is_null($scopeTaxAmount)) { + // Like core's scoped rows: unit_amount = total_amount. + $totalAmount = Tools::ps_round($totalRate > 0 ? ($scopeTaxAmount * $taxRuleGroup['tax_rate'] / $totalRate) : 0, _PS_PRICE_COMPUTE_PRECISION_); + $unitAmount = $totalAmount; + } else { + $unitAmount = Tools::ps_round(($objOrderDetail->unit_price_tax_excl * $taxRuleGroup['tax_rate']) / 100, _PS_PRICE_COMPUTE_PRECISION_); + $totalAmount = Tools::ps_round(($objOrderDetail->total_price_tax_excl * $taxRuleGroup['tax_rate']) / 100, _PS_PRICE_COMPUTE_PRECISION_); + } + $values[] = '(' . (int) $objOrderDetail->id_order . ', ' . $idOrderDetail . ', ' . $idHtlBooking . ', ' + . $idServiceProductOrderDetail . ', ' . $idTax . ', ' . (float) $unitAmount . ', ' . (float) $totalAmount . ', NOW())'; + } + + $sql = 'INSERT INTO `' . _DB_PREFIX_ . 'order_tax_detail` + (id_order, id_order_detail, id_htl_booking, id_service_product_order_detail, id_tax, unit_amount, total_amount, date_add) + VALUES ' . implode(',', $values); + + return Db::getInstance()->execute($sql); + } + + public function createTaxRulesGroup($taxGroupRate, $taxes, $objAddress) + { + if($taxGroupRate) { + $objTaxRulesGroup = new TaxRulesGroup(); + $objTaxRulesGroup->name = 'TAX ('.Tools::ps_round($taxGroupRate, _PS_PRICE_COMPUTE_PRECISION_).'%)'; + $objTaxRulesGroup->deleted = 1; + $objTaxRulesGroup->active = 1; + $objTaxRulesGroup->add(); + + if((!isset($taxes) || !$taxes) && $taxGroupRate) { + $taxes = array( + array( + 'name' => $objTaxRulesGroup->name, + 'rate' => Tools::ps_round($taxGroupRate, _PS_PRICE_COMPUTE_PRECISION_) + )); + } + + $this->createTaxeRulesInGroup($objTaxRulesGroup->id, $taxes, $objAddress); + + return $objTaxRulesGroup->id; + } + return 0; + } + + public function createTaxeRulesInGroup($idTaxRulesGroup, $taxes, $objAddress) + { + if($taxes) { + foreach($taxes as $tax) { + $objTax = new Tax(); + foreach (Language::getLanguages(false) as $language) { + $objTax->name[$language['id_lang']] = substr($tax['name'], 0, 30); + } + + $objTax->rate = $tax['rate']; + $objTax->active = 1; + $objTax->deleted = 1; + $objTax->add(); + + $objTaxRule = new TaxRule(); + $objTaxRule->id_tax = $objTax->id; + $objTaxRule->id_tax_rules_group = $idTaxRulesGroup; + $objTaxRule->id_country = $objAddress->id_country; + $objTaxRule->id_state = $objAddress->id_state; + $objTaxRule->behavior = 1; + + $objTaxRule->add(); + + $this->wsTaxRulesGroup[$idTaxRulesGroup][] = array( + 'tax' => $objTax->id, + 'tax_rate' => $objTax->rate, + 'tax_rule' => $objTaxRule->id + ); + } + } + + } + + /** + * Adding history to the order. + */ + public function addOrderHistory($idOrder, $orderStatus) + { + $objOrder = new Order((int) $idOrder); + $currentOrderStatus = $objOrder->getCurrentOrderState(); + if ($currentOrderStatus->id != $orderStatus->id) { + $objOrderHistory = new OrderHistory(); + $objOrderHistory->id_order = $objOrder->id; + $useExistingsPayment = false; + if (!$objOrder->hasInvoice()) { + $useExistingsPayment = true; + } + + $objOrderHistory->changeIdOrderState((int)$orderStatus->id, $objOrder, $useExistingsPayment); + $objOrderHistory->add(true, array()); + } + } + + public function createGuest() + { + $guest = new Guest(); + $guest->id_operating_system = 7; // For Android Device + $guest->id_web_browser = 1; // For Other(Opera) + $guest->mobile_theme = 1; // For Mobile device + $guest->save(); + if ($guest->id) { + return $guest->id; + } + + return 0; + } + + /** + * Updating the services and their prices. + */ + public function updateServicesInOrder($idOrder) + { + $objOrder = new Order($idOrder); + $objServiceProductOrderDetail = new ServiceProductOrderDetail(); + if (isset($this->wsRequestedRoomTypes) && $this->wsRequestedRoomTypes) { + if ($orderedServices = $objServiceProductOrderDetail->getRoomTypeServiceProducts($objOrder->id, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + null, + null + )) { + foreach ($orderedServices as $orderedService) { + $objHotelBookingDetail = new HotelBookingDetail($orderedService['id_htl_booking_detail']); + $dateRoomJoinKey = strtotime($objHotelBookingDetail->date_from).''.strtotime($objHotelBookingDetail->date_to).$orderedService['id_room_type'].$orderedService['id_room']; + + if (isset($this->wsRequestedRoomTypes[$dateRoomJoinKey]['services']) + && $this->wsRequestedRoomTypes[$dateRoomJoinKey]['services'] + && isset($orderedService['additional_services']) + && $orderedService['additional_services'] + ) { + foreach ($orderedService['additional_services'] as $service) { + $objOrderDetail = new OrderDetail($service['id_order_detail']); + $objServiceProductOrderDetail = new ServiceProductOrderDetail($service['id_service_product_order_detail']); + $quantity = $objServiceProductOrderDetail->quantity * self::getServiceBillableDays( + $objOrderDetail->product_price_calculation_method, + $objHotelBookingDetail->date_from, + $objHotelBookingDetail->date_to + ); + + if (isset($this->wsRequestedRoomTypes[$dateRoomJoinKey]['services'][$service['id_product']])) { + $oldPriceTaxExcl = $objServiceProductOrderDetail->total_price_tax_excl; + $oldPriceTaxIncl = $objServiceProductOrderDetail->total_price_tax_incl; + if ($oldPriceTaxExcl > 0) { + $oldTaxMultiplier = $oldPriceTaxIncl / $oldPriceTaxExcl; + } else { + $oldTaxMultiplier = 1; + } + + if (isset($this->wsRequestedRoomTypes[$dateRoomJoinKey]['services'][$service['id_product']]['unit_price_without_tax'])) { + $unitPriceTaxExcl = $this->wsRequestedRoomTypes[$dateRoomJoinKey]['services'][$service['id_product']]['unit_price_without_tax']; + $unitPriceTaxIncl = $this->wsRequestedRoomTypes[$dateRoomJoinKey]['services'][$service['id_product']]['unit_price_without_tax'] * $oldTaxMultiplier; + $totalPriceTaxExcl = $unitPriceTaxExcl * $quantity; + $totalPriceTaxIncl = $unitPriceTaxIncl * $quantity; + $objServiceProductOrderDetail->unit_price_tax_excl = Tools::ps_round($unitPriceTaxExcl, _PS_PRICE_COMPUTE_PRECISION_); + $objServiceProductOrderDetail->unit_price_tax_incl = Tools::ps_round($unitPriceTaxIncl, _PS_PRICE_COMPUTE_PRECISION_); + $objServiceProductOrderDetail->total_price_tax_excl = Tools::ps_round($totalPriceTaxExcl, _PS_PRICE_COMPUTE_PRECISION_); + $objServiceProductOrderDetail->total_price_tax_incl = Tools::ps_round($totalPriceTaxIncl, _PS_PRICE_COMPUTE_PRECISION_); + + $priceDiffTaxExcl = $objServiceProductOrderDetail->total_price_tax_excl - $oldPriceTaxExcl; + $priceDiffTaxIncl = $objServiceProductOrderDetail->total_price_tax_incl - $oldPriceTaxIncl; + + $objOrderDetail->total_price_tax_excl += $priceDiffTaxExcl; + $objOrderDetail->total_price_tax_incl += $priceDiffTaxIncl; + + $objOrderDetail->unit_price_tax_excl = Tools::ps_round(($objOrderDetail->total_price_tax_excl / $objOrderDetail->product_quantity), _PS_PRICE_COMPUTE_PRECISION_); + $objOrderDetail->unit_price_tax_incl = Tools::ps_round(($objOrderDetail->total_price_tax_incl / $objOrderDetail->product_quantity), _PS_PRICE_COMPUTE_PRECISION_); + + $objOrder->total_paid_tax_excl += $priceDiffTaxExcl; + $objOrder->total_paid_tax_incl += $priceDiffTaxIncl; + $objOrder->total_paid += $priceDiffTaxIncl; + } elseif (isset($this->wsRequestedRoomTypes[$dateRoomJoinKey]['services'][$service['id_product']]['total_price_without_tax'])) { + $totalPriceTaxExcl = $this->wsRequestedRoomTypes[$dateRoomJoinKey]['services'][$service['id_product']]['total_price_without_tax']; + $totalPriceTaxIncl = $this->wsRequestedRoomTypes[$dateRoomJoinKey]['services'][$service['id_product']]['total_price_without_tax'] * $oldTaxMultiplier; + $unitPriceTaxExcl = $quantity > 0 ? $totalPriceTaxExcl / $quantity : 0; + $unitPriceTaxIncl = $quantity > 0 ? $totalPriceTaxIncl / $quantity : 0; + + $objServiceProductOrderDetail->unit_price_tax_excl = Tools::ps_round($unitPriceTaxExcl, _PS_PRICE_COMPUTE_PRECISION_); + $objServiceProductOrderDetail->unit_price_tax_incl = Tools::ps_round($unitPriceTaxIncl, _PS_PRICE_COMPUTE_PRECISION_); + $objServiceProductOrderDetail->total_price_tax_excl = Tools::ps_round($totalPriceTaxExcl, _PS_PRICE_COMPUTE_PRECISION_); + $objServiceProductOrderDetail->total_price_tax_incl = Tools::ps_round($totalPriceTaxIncl, _PS_PRICE_COMPUTE_PRECISION_); + + $priceDiffTaxExcl = Tools::ps_round($objServiceProductOrderDetail->total_price_tax_excl - $oldPriceTaxExcl, _PS_PRICE_COMPUTE_PRECISION_); + $priceDiffTaxIncl = Tools::ps_round($objServiceProductOrderDetail->total_price_tax_incl - $oldPriceTaxIncl, _PS_PRICE_COMPUTE_PRECISION_); + + $objOrderDetail->total_price_tax_excl += $priceDiffTaxExcl; + $objOrderDetail->total_price_tax_incl += $priceDiffTaxIncl; + + $objOrderDetail->unit_price_tax_excl = Tools::ps_round(($objOrderDetail->total_price_tax_excl / $objOrderDetail->product_quantity), _PS_PRICE_COMPUTE_PRECISION_); + $objOrderDetail->unit_price_tax_incl = Tools::ps_round(($objOrderDetail->total_price_tax_incl / $objOrderDetail->product_quantity), _PS_PRICE_COMPUTE_PRECISION_); + + $objOrder->total_paid_tax_excl += $priceDiffTaxExcl; + $objOrder->total_paid_tax_incl += $priceDiffTaxIncl; + $objOrder->total_paid += $priceDiffTaxIncl; + } + } + + if (isset($this->wsRequestedRoomTypes[$dateRoomJoinKey]['services'][$service['id_product']]['id_tax_rules_group'])) { + $oldPriceTaxIncl = Tools::ps_round($objServiceProductOrderDetail->total_price_tax_incl, _PS_PRICE_COMPUTE_PRECISION_) ; + if (isset($this->wsRequestedRoomTypes[$dateRoomJoinKey]['services'][$service['id_product']]['total_tax']) + && $this->wsRequestedRoomTypes[$dateRoomJoinKey]['services'][$service['id_product']]['total_tax'] + ) { + // Use the sent total_tax, not a recomputed rate; the group is kept for tax rows. + $unitPriceTaxIncl = $objServiceProductOrderDetail->unit_price_tax_excl + + ($quantity > 0 ? ($this->wsRequestedRoomTypes[$dateRoomJoinKey]['services'][$service['id_product']]['total_tax'] / $quantity) : 0); + } else { + $objAddress = new Address((int) $objOrder->id_address_tax); + $objTaxManager = TaxManagerFactory::getManager($objAddress, $this->wsRequestedRoomTypes[$dateRoomJoinKey]['services'][$service['id_product']]['id_tax_rules_group']); + $objTaxCalculator = $objTaxManager->getTaxCalculator(); + $unitPriceTaxIncl = $objTaxCalculator->addTaxes($objServiceProductOrderDetail->unit_price_tax_excl); + } + + $objServiceProductOrderDetail->unit_price_tax_incl = Tools::ps_round($unitPriceTaxIncl, _PS_PRICE_COMPUTE_PRECISION_); + $objServiceProductOrderDetail->total_price_tax_incl = Tools::ps_round(($unitPriceTaxIncl * $quantity), _PS_PRICE_COMPUTE_PRECISION_); + + $priceDiffTaxIncl = $objServiceProductOrderDetail->total_price_tax_incl - $oldPriceTaxIncl; + + $objOrderDetail->total_price_tax_incl += $priceDiffTaxIncl; + $objOrderDetail->unit_price_tax_incl = Tools::ps_round(($objOrderDetail->total_price_tax_incl / $objOrderDetail->product_quantity), _PS_PRICE_COMPUTE_PRECISION_); + + $objOrder->total_paid_tax_incl += $priceDiffTaxIncl; + $objOrder->total_paid += $priceDiffTaxIncl; + } elseif (isset($this->wsRequestedRoomTypes[$dateRoomJoinKey]['services'][$service['id_product']]['total_tax']) + && $this->wsRequestedRoomTypes[$dateRoomJoinKey]['services'][$service['id_product']]['total_tax'] + ) { + $oldPriceTaxIncl = $objServiceProductOrderDetail->total_price_tax_incl; + $objServiceProductOrderDetail->total_price_tax_incl = $objServiceProductOrderDetail->total_price_tax_excl + $this->wsRequestedRoomTypes[$dateRoomJoinKey]['services'][$service['id_product']]['total_tax']; + + $objServiceProductOrderDetail->unit_price_tax_incl = Tools::ps_round($quantity > 0 ? ($objServiceProductOrderDetail->total_price_tax_incl / $quantity) : 0, _PS_PRICE_COMPUTE_PRECISION_); + $objServiceProductOrderDetail->total_price_tax_incl = Tools::ps_round(($objServiceProductOrderDetail->total_price_tax_incl), _PS_PRICE_COMPUTE_PRECISION_); + + $priceDiffTaxIncl = $objServiceProductOrderDetail->total_price_tax_incl - $oldPriceTaxIncl; + $objOrderDetail->total_price_tax_incl += $priceDiffTaxIncl; + $objOrderDetail->unit_price_tax_incl = Tools::ps_round(($objOrderDetail->total_price_tax_incl / $objOrderDetail->product_quantity), _PS_PRICE_COMPUTE_PRECISION_); + + $objOrder->total_paid_tax_incl += $priceDiffTaxIncl; + $objOrder->total_paid += $priceDiffTaxIncl; + } + + $objServiceProductOrderDetail->save(); + $objOrderDetail->save(); + + $isAutoAdded = false; + if ($objOrderDetail->product_auto_add && $objOrderDetail->product_price_addition_type == Product::PRICE_ADDITION_TYPE_WITH_ROOM) { + $isAutoAdded = true; + } + + if (!$isAutoAdded + && isset($this->wsRequestedRoomTypes[$dateRoomJoinKey]['services'][$service['id_product']]['id_tax_rules_group']) + && $this->wsRequestedRoomTypes[$dateRoomJoinKey]['services'][$service['id_product']]['id_tax_rules_group'] + ) { + $this->saveTaxCalculator($objOrderDetail->id, $this->wsRequestedRoomTypes[$dateRoomJoinKey]['services'][$service['id_product']]['id_tax_rules_group'], 0, $objServiceProductOrderDetail->id); + } + } + } + } + + $objOrder->save(); + } + + $objHotelBookingDetail = new HotelBookingDetail(); + if ($orderedRooms = $objHotelBookingDetail->getOrderCurrentDataByOrderId($objOrder->id)) { + foreach ($orderedRooms as $orderedRoom) { + $dateRoomJoinKey = strtotime($orderedRoom['date_from']).''.strtotime($orderedRoom['date_to']).$orderedRoom['id_product'].$orderedRoom['id_room']; + if(isset($this->wsRequestedRoomTypes[$dateRoomJoinKey]['room']) && $this->wsRequestedRoomTypes[$dateRoomJoinKey]['room']) { + $objHotelBookingDetail = new HotelBookingDetail((int)$orderedRoom['id']); + $objHotelBookingDetail->rate_plan_name = isset($this->wsRequestedRoomTypes[$dateRoomJoinKey]['room']['rate_plan_name']) ? $this->wsRequestedRoomTypes[$dateRoomJoinKey]['room']['rate_plan_name'] : 'Not Available'; + $objHotelBookingDetail->save(); + } + } + } + } + } + + /** + * Recalculates the order total from its actual stored components. + * This is the SINGLE SOURCE OF TRUTH for order pricing. + * Used by BOTH booking creation and modification flows. + * + * Uses core classes ONLY - no raw SQL queries. + * + * Components: + * - Base price + Taxes: from HotelBookingDetail (rooms) + * - Services: from ServiceProductOrderDetail + * - Discounts: from OrderCartRule + * + * @param int $idOrder + * @return float The recalculated total paid tax incl + */ + public function recalculateOrderFromComponents($idOrder) + { + $objOrder = new Order($idOrder); + $objHotelBookingDetail = new HotelBookingDetail(); + $objServiceProductOrderDetail = new ServiceProductOrderDetail(); + + // 1. Get room totals from HotelBookingDetail (active bookings only) + $roomsTotalTaxIncl = 0; + $roomsTotalTaxExcl = 0; + $activeBookingIds = array(); + $activeBookingKeys = array(); + if ($bookings = $objHotelBookingDetail->getBookingDataByOrderId($idOrder)) { + // Refunds come from completed order returns, cancellations from id_status. The old + // is_refunded/is_cancelled columns (shops upgraded from 1.7) are no longer written, + // so they only add rooms flagged before the upgrade. + $refundedBookingKeys = array(); + if (method_exists('OrderReturn', 'getRefundedBookingIdsSubquery')) { + if ($refundedBookings = Db::getInstance()->executeS( + OrderReturn::getRefundedBookingIdsSubquery().' AND orr.`id_order` = '.(int) $idOrder + )) { + foreach ($refundedBookings as $refundedBooking) { + $refundedBookingKeys[$refundedBooking['id_htl_booking']] = true; + } + } + } + + foreach ($bookings as $booking) { + $isRefunded = isset($refundedBookingKeys[$booking['id']]) || !empty($booking['is_refunded']); + $isCancelled = (defined('HotelBookingDetail::STATUS_CANCELLED') + && $booking['id_status'] == HotelBookingDetail::STATUS_CANCELLED) + || !empty($booking['is_cancelled']); + if (!$isRefunded && !$isCancelled) { + $roomsTotalTaxIncl += (float) $booking['total_price_tax_incl']; + $roomsTotalTaxExcl += (float) $booking['total_price_tax_excl']; + $activeBookingIds[] = $booking['id']; + $activeBookingKeys[$booking['id']] = true; + } + } + } + + // 2. Get service totals from ServiceProductOrderDetail (active bookings only) + $servicesTotalTaxIncl = 0; + $servicesTotalTaxExcl = 0; + if ($services = $objServiceProductOrderDetail->getServiceProductsInOrder($idOrder)) { + foreach ($services as $service) { + if (isset($service['id_htl_booking_detail']) + && isset($activeBookingKeys[$service['id_htl_booking_detail']]) + ) { + $servicesTotalTaxIncl += (float) $service['total_price_tax_incl']; + $servicesTotalTaxExcl += (float) $service['total_price_tax_excl']; + } + } + } + + // 3. Get discount totals from Order cart rules + $totalDiscountsTaxIncl = (float) $objOrder->getCartRulesTotal(true); + $totalDiscountsTaxExcl = (float) $objOrder->getCartRulesTotal(false); + + // 4. Calculate final totals + $totalProductsTaxIncl = $roomsTotalTaxIncl + $servicesTotalTaxIncl; + $totalProductsTaxExcl = $roomsTotalTaxExcl + $servicesTotalTaxExcl; + $totalPaidTaxIncl = Tools::ps_round($totalProductsTaxIncl - $totalDiscountsTaxIncl, _PS_PRICE_COMPUTE_PRECISION_); + $totalPaidTaxExcl = Tools::ps_round($totalProductsTaxExcl - $totalDiscountsTaxExcl, _PS_PRICE_COMPUTE_PRECISION_); + + // 5. Update order totals + $objOrder->total_products = $totalProductsTaxExcl; + $objOrder->total_products_wt = $totalProductsTaxIncl; + $objOrder->total_paid_tax_excl = $totalPaidTaxExcl; + $objOrder->total_paid_tax_incl = $totalPaidTaxIncl; + $objOrder->total_paid = $totalPaidTaxIncl; + + // If payment is completed, total_paid_real should match + if ($objOrder->current_state == Configuration::get('PS_OS_PAYMENT_ACCEPTED')) { + $objOrder->total_paid_real = $totalPaidTaxIncl; + } + + $objOrder->update(); + + // Sync invoice totals (the module uses one invoice per order). + if ($orderInvoice = $objOrder->getInvoicesCollection()->getFirst()) { + $orderInvoice->total_products = $totalProductsTaxExcl; + $orderInvoice->total_products_wt = $totalProductsTaxIncl; + $orderInvoice->total_paid_tax_excl = $totalPaidTaxExcl; + $orderInvoice->total_paid_tax_incl = $totalPaidTaxIncl; + $orderInvoice->update(); + } + + return $totalPaidTaxIncl; + } + + /** + * Adjusts the order price when the external total_price_with_tax differs + * from the calculated total. Uses cart rules for differences rather than + * modifying room/service base prices. + * + * @param int $idOrder + * @param array $params + */ + public function manageOrderPrice($idOrder, $params) + { + $objOrder = new Order($idOrder); + if (isset($params['price_details']['total_price_with_tax']) && (float) $params['price_details']['total_price_with_tax']) { + if ((float) $params['price_details']['total_price_with_tax'] < Tools::ps_round($objOrder->total_paid_tax_incl, _PS_PRICE_COMPUTE_PRECISION_)) { + $cartRule['code'] = Tools::passwdGen(8, 'NO_NUMERIC'); + $cartRule['currency'] = ''; + $cartRule['value'] = $objOrder->total_paid_tax_incl - (float) $params['price_details']['total_price_with_tax']; + if ($cartRule['value'] && $objOrder->getInvoicesCollection() && count($objOrder->getInvoicesCollection())) { + $cartRule['value'] /= count($objOrder->getInvoicesCollection()); + } + + $cartRule['type'] = self::API_CART_RULE_VALUE_TYPE_AMOUNT; + $this->addCartRulesToOrder(array($cartRule), $objOrder->id); + } else if ((float) $params['price_details']['total_price_with_tax'] > Tools::ps_round($objOrder->total_paid_tax_incl, _PS_PRICE_COMPUTE_PRECISION_)) { + $difference = (float) $params['price_details']['total_price_with_tax'] - Tools::ps_round($objOrder->total_paid_tax_incl, _PS_PRICE_COMPUTE_PRECISION_); + if ($difference > 0) { + $roomsInOrder = (new HotelBookingDetail())->getOrderCurrentDataByOrderId($idOrder); + if (empty($roomsInOrder)) { + $roomsInOrder = array(); + } + $firstRoomInOrder = array_shift($roomsInOrder); + if ($firstRoomInOrder) { + $service = array(); + $service['name'] = 'Created By API'; + $service['total_price_without_tax'] = $difference; + $service['total_price_with_tax'] = $difference; + $service['total_tax'] = 0; + $service['price_mode'] = self::API_SERVICE_PRICE_MODE_PER_BOOKING; + $service['quantity'] = 1; + $service['is_new'] = 1; + $servicesToAdd = array($service); + $this->addServicesInOrderedRoom($servicesToAdd, $firstRoomInOrder['id']); + } + } + } + } + + $paymentStatus = isset($params['payment_status']) ? $params['payment_status'] : ''; + if ($paymentStatus == self::API_BOOKING_PAYMENT_STATUS_COMPLETED) { + $this->managePaymentHistory($objOrder->reference, $params['price_details']['total_price_with_tax']); + } elseif ($paymentStatus == self::API_BOOKING_PAYMENT_STATUS_PARTIAL + && ($sentPaidAmount = $this->getSentPaidAmount($params)) !== null + && isset($params['price_details']['total_price_with_tax']) + ) { + // Trim payments above the amount actually paid. + $this->managePaymentHistory( + $objOrder->reference, + min($sentPaidAmount, (float) $params['price_details']['total_price_with_tax']) + ); + } + } + + /** + * Adding the required guest informations into the cart. + */ + public function processGuestDetails($guestDetails) + { + if (isset($guestDetails['id_customer']) + && Validate::isLoadedObject($objCustomer = new Customer((int) $guestDetails['id_customer'])) + ) { + $this->bookingCustomer = $objCustomer; + } else { + $objCustomer = new Customer(); + $this->bookingCustomer = $objCustomer->getByEmail($guestDetails['email'], null, false); + } + + if (isset($this->bookingCustomer->id) + && $this->bookingCustomer->id + ) { + $idGuest = Guest::getFromCustomer($this->bookingCustomer->id); + } else { + $idGuest = $this->createGuest(); + } + + $this->context->cart->id_guest = $idGuest; + } + + /** + * Adding the cart language. + */ + public function processLanguage($params) + { + $idLang = Configuration::get('PS_LANG_DEFAULT'); + if (isset($params['id_language']) + && Validate::isLoadedObject($objLanguage = new Language((int) $params['id_language'])) + && $objLanguage->active + ) { + $idLang = $objLanguage->id; + } + + $this->context->language = new Language((int) $idLang); + $this->context->cart->id_lang = $idLang; + } + + /** + * Adding the cart currency. + */ + public function processCurrency($params) + { + $idCurrency = Configuration::get('PS_CURRENCY_DEFAULT'); + if (isset($params['currency']) + && ($selectedCurrency = Currency::getIdByIsoCode($params['currency'])) + && Validate::isLoadedObject($objCurrency = new Currency($selectedCurrency)) + && $objCurrency->active + ) { + $idCurrency = $selectedCurrency; + } + + $this->context->currency = new Currency((int) $idCurrency); + $this->context->cart->id_currency = $idCurrency; + } + + public function formatRoomTypesInRequestData($data) + { + $roomTypes = $data; + if (count($roomTypes)) { + $formattedRoomTypes = array(); + foreach ($roomTypes as $roomTypeKey => $roomType) { + $dateProductJoinKey = $roomType['id_room_type'].'_'.strtotime($roomType['check_in_date']).strtotime($roomType['check_out_date']); + + if (!isset($formattedRoomTypes[$dateProductJoinKey])) { + $formattedRoomTypes[$dateProductJoinKey] = $roomType; + } else { + $formattedRoomTypes[$dateProductJoinKey]['number_of_rooms'] += $roomType['number_of_rooms']; + } + + if (isset($roomType['rooms'])) { + $formattedRoomTypes[$dateProductJoinKey]['rooms'] = $this->formatRoomInRequestData($roomType); + } + } + + $roomTypes = $formattedRoomTypes; + } + + return $roomTypes; + } + + public function formatServicesInRequestData($data) + { + $selectedServices = array(); + if (isset($data['services']['service'][0])) { + $selectedServices = $data['services']['service']; + } else if (isset($data['services']) + && !isset($data['services'][0]) + && isset($data['services']['service']) + ) { + $selectedServices[] = $data['services']['service']; + } else if (isset($data['services']) && isset($data['services'][0])) { + $selectedServices = $data['services']; + } + + $formattedServices = array(); + foreach ($selectedServices as $service) { + $key = isset($service['id_service']) ? $service['id_service'] : 'new_'.rand(); + if (isset($service['id_service'])) { + $formattedServices[$key]['quantity'] = isset($service['quantity']) ? $service['quantity'] : 1; + $formattedServices[$key]['id_product'] = $service['id_service']; + if (isset($service['unit_price_without_tax'])) { + $formattedServices[$key]['unit_price_without_tax'] = $service['unit_price_without_tax']; + } else if (isset($service['total_price_without_tax'])) { + $formattedServices[$key]['total_price_without_tax'] = $service['total_price_without_tax']; + } + + if (isset($service['id_tax_rules_group'])) { + $formattedServices[$key]['id_tax_rules_group'] = $service['id_tax_rules_group']; + } else if ($service['total_tax']) { + $formattedServices[$key]['total_tax'] = $service['total_tax']; + } + } else { + $service['is_new'] = true; + $service['quantity'] = isset($service['quantity']) ? $service['quantity'] : 1; + $formattedServices[$key] = $service; + } + } + + return $formattedServices; + } + + public function formatRoomInRequestData($data) + { + $rooms = array(); + if (isset($data['rooms']['room'][0])) { + $rooms = $data['rooms']['room']; + } else if (isset($data['rooms']) + && !isset($data['rooms'][0]) + && isset($data['rooms']['room']) + ) { + $rooms[] = $data['rooms']['room']; + } else if (isset($data['rooms']) && isset($data['rooms'][0])) { + $rooms = $data['rooms']; + } + + if (count($rooms)) { + $formattedRooms = array(); + foreach ($rooms as $roomKey => $room) { + $selectedServices = $this->formatServicesInRequestData($room); + + $occupancy = array( + 'adults' => $room['occupancy']['adults'], + 'children' => $room['occupancy']['children'], + 'child_ages' => $room['occupancy']['children_ages'] + ); + + $rooms[$roomKey]['services'] = $selectedServices; + $rooms[$roomKey]['occupancy'] = $occupancy; + $key = $roomKey; + if (isset($room['id_room'])) { + $key = 'r_'.$room['id_room']; + } + + $formattedRooms[$key] = $room; + $formattedRooms[$key]['services'] = $selectedServices; + $formattedRooms[$key]['occupancy'] = $occupancy; + + + if (isset($room['id_tax_rules_group'])) { + $formattedRooms[$key]['id_tax_rules_group'] = $room['id_tax_rules_group']; + } else if ($room['total_tax']) { + $formattedRooms[$key]['total_tax'] = $room['total_tax']; + } + } + + $rooms = $formattedRooms; + } + + return $rooms; + } + + /** + * Processing the PUT request. + */ + public function handlePutRequest($params) + { + $this->currentBookingId = (int) $params['id_booking']; + + QloCMConnector::$idBookingCm = isset($params['id_channel_manager_booking']) && $params['id_channel_manager_booking'] + ? $params['id_channel_manager_booking'] + : uniqid('CM_API_', true); + + $this->processCurrency($params); + $this->processGuestDetails($params['guest_detail']); + $this->processCustomer($params['guest_detail']); + $objOrder = new Order((int) $params['id_booking']); + $objHotelBookingDetail = new HotelBookingDetail(); + $roomsToRemove = array(); + $roomsToAdd = array(); + $roomsToUpdate = array(); + $roomTypes = $this->formatRoomTypesInRequestData($params['room_bookings']); + $isReversal = in_array($params['booking_status'], array(self::API_BOOKING_STATUS_CANCELLED, self::API_BOOKING_STATUS_REFUNDED)); + + // The CM sends one request per room type (same cmid): only change the requested room types. + $requestedRoomTypeIds = array(); + if (!empty($params['room_bookings']) && is_array($params['room_bookings'])) { + foreach ($params['room_bookings'] as $roomBooking) { + if (isset($roomBooking['id_room_type'])) { + $requestedRoomTypeIds[(int) $roomBooking['id_room_type']] = true; + } + } + } + $requestedRoomTypeRooms = array(); + $otherRoomTypeRooms = array(); + if ($orderRooms = $objHotelBookingDetail->getOrderCurrentDataByOrderId($objOrder->id)) { + foreach ($orderRooms as $orderRoom) { + if (!$requestedRoomTypeIds || isset($requestedRoomTypeIds[(int) $orderRoom['id_product']])) { + $requestedRoomTypeRooms[] = $orderRoom; + } else { + $otherRoomTypeRooms[] = $orderRoom; + } + } + } + + if ($isReversal && $otherRoomTypeRooms) { + // Partial cancel: drop the requested room types' lines, keep the order active. + if ($requestedRoomTypeRooms) { + $this->removeRoomLineFromOrder($params, $requestedRoomTypeRooms); + $this->recalculateOrderFromComponents($objOrder->id); + $objOrder = new Order($objOrder->id); + $this->managePaymentHistory($objOrder->reference, $objOrder->total_paid_tax_incl); + } + $this->deleteWsFeaturePrices(); + QloCMConnector::$idBookingCm = null; + $this->getBookingDetails($objOrder->id); + + return; + } + + if (!$isReversal + && $otherRoomTypeRooms + && isset($params['price_details']['total_price_with_tax']) + ) { + // price_details covers only the requested room types; add the other room types' current share. + $requestedTotalTaxIncl = 0; + $requestedTotalTaxExcl = 0; + $requestedBookingIds = array(); + foreach ($requestedRoomTypeRooms as $orderRoom) { + $requestedTotalTaxIncl += (float) $orderRoom['total_price_tax_incl']; + $requestedTotalTaxExcl += (float) $orderRoom['total_price_tax_excl']; + $requestedBookingIds[] = (int) $orderRoom['id']; + } + if ($requestedBookingIds) { + $requestedServicesTotal = Db::getInstance()->getRow( + 'SELECT IFNULL(SUM(`total_price_tax_incl`), 0) AS total_incl, IFNULL(SUM(`total_price_tax_excl`), 0) AS total_excl + FROM `'._DB_PREFIX_.'service_product_order_detail` + WHERE `id_htl_booking_detail` IN ('.implode(',', $requestedBookingIds).')' + ); + $requestedTotalTaxIncl += (float) $requestedServicesTotal['total_incl']; + $requestedTotalTaxExcl += (float) $requestedServicesTotal['total_excl']; + } + $otherTotalTaxIncl = (float) $objOrder->total_paid_tax_incl - $requestedTotalTaxIncl; + $otherTotalTaxExcl = (float) $objOrder->total_paid_tax_excl - $requestedTotalTaxExcl; + $params['price_details']['total_price_with_tax'] = Tools::ps_round( + (float) $params['price_details']['total_price_with_tax'] + $otherTotalTaxIncl, + _PS_PRICE_COMPUTE_PRECISION_ + ); + $params['price_details']['total_tax'] = Tools::ps_round( + (isset($params['price_details']['total_tax']) ? (float) $params['price_details']['total_tax'] : 0) + + ($otherTotalTaxIncl - $otherTotalTaxExcl), + _PS_PRICE_COMPUTE_PRECISION_ + ); + } + + if (!$isReversal) { + if ($roomsInOrder = $requestedRoomTypeRooms) { + // Split room: one OTA stay stored as consecutive segments on different rooms. + $splitSegments = $this->getSplitRoomSegments($roomsInOrder, $roomTypes); + $splitChainRooms = array(); + $splitChainSegmentsLeft = array(); + foreach ($splitSegments as $splitSegment) { + $splitChainSegmentsLeft[$splitSegment['chain']] = $splitSegment['chain_segments']; + } + + foreach ($roomsInOrder as $orderRoomKey => $orderRoom) { + $isSplitRoom = isset($splitSegments[$orderRoomKey]); + $requestedRoomTypeKey = $isSplitRoom + ? $splitSegments[$orderRoomKey]['room_type_key'] + : $orderRoom['id_product'].'_'.strtotime($orderRoom['date_from']).strtotime($orderRoom['date_to']); + if (!$isSplitRoom && !isset($roomTypes[$requestedRoomTypeKey]) + || !isset($roomTypes[$requestedRoomTypeKey]['number_of_rooms']) + || $roomTypes[$requestedRoomTypeKey]['number_of_rooms'] <= 0 + ) { + $requestedRoomTypeKey = false; + $room_key = 'r_'.$orderRoom['id_room']; + foreach ($roomTypes as $roomTypeKey => $roomType) { + if ((int) $roomType['id_room_type'] != (int) $orderRoom['id_product']) { + continue; + } + + if (!isset($roomType['number_of_rooms']) || $roomType['number_of_rooms'] <= 0) { + continue; + } + + if (isset($roomType['rooms'][$room_key])) { + $requestedRoomTypeKey = $roomTypeKey; + break; + } + // Fallback for same room type without room-wise mapping. + if ($requestedRoomTypeKey === false) { + $requestedRoomTypeKey = $roomTypeKey; + } + } + } + + if (!isset($roomTypes[$requestedRoomTypeKey]) + || !isset($roomTypes[$requestedRoomTypeKey]['number_of_rooms']) + || $roomTypes[$requestedRoomTypeKey]['number_of_rooms'] <= 0 + ) { + continue; + } + + $requestedCheckIn = date('Y-m-d', strtotime($roomTypes[$requestedRoomTypeKey]['check_in_date'])); + $requestedCheckOut = date('Y-m-d', strtotime($roomTypes[$requestedRoomTypeKey]['check_out_date'])); + $room = array(); + // if there are multiple rooms then there can be a room at index 1 so we are adding a prifix for the below condition. + $room_key = 'r_'.$orderRoom['id_room']; + if ($isSplitRoom && isset($splitChainRooms[$splitSegments[$orderRoomKey]['chain']])) { + // every segment of a split stay shares the one requested room + $room = $splitChainRooms[$splitSegments[$orderRoomKey]['chain']]; + } else { + if (isset($roomTypes[$requestedRoomTypeKey]['rooms'][$room_key])) { + $room = $roomTypes[$requestedRoomTypeKey]['rooms'][$room_key]; + unset($roomTypes[$requestedRoomTypeKey]['rooms'][$room_key]); + } elseif (isset($roomTypes[$requestedRoomTypeKey]['rooms'][0])) { + $room = array_shift($roomTypes[$requestedRoomTypeKey]['rooms']); + } + if ($isSplitRoom) { + $splitChainRooms[$splitSegments[$orderRoomKey]['chain']] = $room; + } + } + + if ($room) { + if ($isSplitRoom) { + // For split rooms, preserve segment dates and distribute price proportionally + $segmentCheckIn = date('Y-m-d', strtotime($orderRoom['date_from'])); + $segmentCheckOut = date('Y-m-d', strtotime($orderRoom['date_to'])); + $room['check_in_date'] = $segmentCheckIn; + $room['check_out_date'] = $segmentCheckOut; + $room['date_from'] = $segmentCheckIn; + $room['date_to'] = $segmentCheckOut; + + // Calculate proportional price for this split segment + $requestedTotalPriceTaxExcl = 0; + if (isset($room['unit_price_without_tax'])) { + $requestedTotalPriceTaxExcl = $room['unit_price_without_tax']; + } else if (isset($room['total_price_with_tax']) && isset($room['total_tax'])) { + $requestedTotalPriceTaxExcl = $room['total_price_with_tax'] - $room['total_tax']; + } + $requestedTotalTax = isset($room['total_tax']) ? floatval($room['total_tax']) : 0; + + // Total nights of the split stay this segment belongs to + $totalSplitNights = $splitSegments[$orderRoomKey]['chain_nights']; + if (!$totalSplitNights) { + $totalSplitNights = 1; + } + + $segmentNights = HotelHelper::getNumberOfDays($orderRoom['date_from'], $orderRoom['date_to']); + if (!$segmentNights) { + $segmentNights = 1; + } + + // Distribute price proportionally by nights + $segmentPriceTaxExcl = Tools::ps_round( + ($requestedTotalPriceTaxExcl / $totalSplitNights) * $segmentNights, + _PS_PRICE_COMPUTE_PRECISION_ + ); + $segmentTax = Tools::ps_round( + ($requestedTotalTax / $totalSplitNights) * $segmentNights, + _PS_PRICE_COMPUTE_PRECISION_ + ); + + $room['unit_price_without_tax'] = $segmentPriceTaxExcl; + $room['total_price_with_tax'] = $segmentPriceTaxExcl + $segmentTax; + $room['total_tax'] = $segmentTax; + } else { + $room['check_in_date'] = $requestedCheckIn; + $room['check_out_date'] = $requestedCheckOut; + $room['date_from'] = $requestedCheckIn; + $room['date_to'] = $requestedCheckOut; + } + $roomsInOrder[$orderRoomKey]['requested_check_in_date'] = $room['check_in_date']; + $roomsInOrder[$orderRoomKey]['requested_check_out_date'] = $room['check_out_date']; + // update the room only if there is room wise breakdown. + $roomsToUpdate[$requestedRoomTypeKey]['requested'][$orderRoom['id_room']] = $room; + $roomsToUpdate[$requestedRoomTypeKey]['order'][$orderRoom['id_room']] = $roomsInOrder[$orderRoomKey]; + } else { + $roomsInOrder[$orderRoomKey]['requested_check_in_date'] = $requestedCheckIn; + $roomsInOrder[$orderRoomKey]['requested_check_out_date'] = $requestedCheckOut; + $roomsToUpdate[$requestedRoomTypeKey]['order'][$orderRoom['id_room']] = $roomsInOrder[$orderRoomKey]; + } + + // A split stay uses one requested room slot: release it with its last segment. + if ($isSplitRoom) { + $splitChainSegmentsLeft[$splitSegments[$orderRoomKey]['chain']]--; + } + + if (!$isSplitRoom || $splitChainSegmentsLeft[$splitSegments[$orderRoomKey]['chain']] <= 0) { + $roomTypes[$requestedRoomTypeKey]['number_of_rooms']--; + if ($roomTypes[$requestedRoomTypeKey]['number_of_rooms'] <= 0) { + unset($roomTypes[$requestedRoomTypeKey]); + } + } + + unset($roomsInOrder[$orderRoomKey]); + } + + $roomsToRemove = $roomsInOrder; + } + + $roomsToAdd = $roomTypes; + if (!$this->validateRequestedRoomTypes($params['room_bookings'], $params['id_property'], $params['id_booking'])) { + QloCMConnector::$idBookingCm = null; + throw new WebserviceException( + ($this->error_msg ?: Tools::displayError('Requested room(s) not available')), + array(404, 400) + ); + } + + $this->resolveRoomExtensionAvailability($roomsToUpdate, $roomsToAdd, $objOrder->id); + } + + // Adding new rooms in the booking + if (count($roomsToAdd)) { + if (!$this->addRoomsInOrder($objOrder->id, $roomsToAdd)) { + QloCMConnector::$idBookingCm = null; + throw new WebserviceException( + ($this->error_msg ?: Tools::displayError('Requested room(s) not available')), + array(404, 400) + ); + } + } + + $objAddress = new Address((int) $objOrder->id_address_tax); + // only perform any update if request is valid. + // Update the information for the services that were updated in the existing rooms + if (count($roomsToUpdate)) { + $objServiceProductOrderDetail = new ServiceProductOrderDetail(); + foreach ($roomsToUpdate as $roomsByDate) { + if (isset($roomsByDate['requested']) && $roomsByDate['requested']) { + foreach ($roomsByDate['requested'] as $roomsKey => $room) { + $idOrder = $roomsByDate['order'][$roomsKey]['id_order']; + $idProduct = $roomsByDate['order'][$roomsKey]['id_product']; + $idRoom = $roomsByDate['order'][$roomsKey]['id_room']; + $dateFrom = $roomsByDate['order'][$roomsKey]['date_from']; + $dateTo = $roomsByDate['order'][$roomsKey]['date_to']; + $dateRoomJoinKey = strtotime($dateFrom).strtotime($dateTo).$idProduct.$idRoom; + + + if(Validate::isLoadedObject($objAddress)){ + $totalPriceTaxExcl = $room['total_price_with_tax'] - $room['total_tax']; + $roomTaxRate = $totalPriceTaxExcl > 0 ? ($room['total_tax']/$totalPriceTaxExcl)*100 : 0; + if(isset($room['taxes']) && $room['taxes']) { + $room['id_tax_rules_group'] = $this->createTaxRulesGroup($roomTaxRate, $room['taxes'], $objAddress); + } else { + $room['id_tax_rules_group'] = $this->createTaxRulesGroup($roomTaxRate, false, $objAddress); + } + } + + if (isset($room['id_tax_rules_group']) && Validate::isLoadedObject(new TaxRulesGroup((int) $room['id_tax_rules_group']))) { + $this->wsRequestedRooms[$dateRoomJoinKey]['id_tax_rules_group'] = $room['id_tax_rules_group']; + } else if (!empty($room['total_tax'])) { + $totalPriceTaxExcl = $room['total_price_with_tax'] - $room['total_tax']; + $roomTaxRate = $totalPriceTaxExcl > 0 ? ($room['total_tax']/$totalPriceTaxExcl)*100 : 0; + $this->wsRequestedRooms[$dateRoomJoinKey]['id_tax_rules_group'] = $this->createTaxRulesGroup($roomTaxRate, $room['taxes'], $objAddress); + } + + $requestedCheckInDate = isset($room['check_in_date']) + ? date('Y-m-d', strtotime($room['check_in_date'])) + : date('Y-m-d', strtotime($roomsByDate['order'][$roomsKey]['date_from'])); + $requestedCheckOutDate = isset($room['check_out_date']) + ? date('Y-m-d', strtotime($room['check_out_date'])) + : date('Y-m-d', strtotime($roomsByDate['order'][$roomsKey]['date_to'])); + $currentCheckInDate = date('Y-m-d', strtotime($roomsByDate['order'][$roomsKey]['date_from'])); + $currentCheckOutDate = date('Y-m-d', strtotime($roomsByDate['order'][$roomsKey]['date_to'])); + + $isPriceChanged = isset($room['total_price_with_tax']) + && ((float) $room['total_price_with_tax']) != ((float) $roomsByDate['order'][$roomsKey]['total_price_tax_incl']); + $isDateChanged = ($requestedCheckInDate !== $currentCheckInDate) + || ($requestedCheckOutDate !== $currentCheckOutDate); + + if ($isPriceChanged || $isDateChanged) { + $this->updateRoomPriceInOrder($room, $roomsByDate['order'][$roomsKey]); + $this->wsRequestedRoomTypes[$dateRoomJoinKey]['room'] = $room; + } + + if ($isDateChanged) { + // Later steps (tax pass, services) look rooms up by their current dates. + $newDateRoomJoinKey = strtotime($requestedCheckInDate).strtotime($requestedCheckOutDate).$idProduct.$idRoom; + if (isset($this->wsRequestedRooms[$dateRoomJoinKey])) { + $this->wsRequestedRooms[$newDateRoomJoinKey] = $this->wsRequestedRooms[$dateRoomJoinKey]; + unset($this->wsRequestedRooms[$dateRoomJoinKey]); + } + if (isset($this->wsRequestedRoomTypes[$dateRoomJoinKey])) { + $this->wsRequestedRoomTypes[$newDateRoomJoinKey] = $this->wsRequestedRoomTypes[$dateRoomJoinKey]; + unset($this->wsRequestedRoomTypes[$dateRoomJoinKey]); + } + $dateRoomJoinKey = $newDateRoomJoinKey; + } + + $idHotelBookingDetail = $roomsByDate['order'][$roomsKey]['id']; + if ($existingServices = $objServiceProductOrderDetail->getRoomTypeServiceProducts( + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + null, + null, + null, + 0, + $idHotelBookingDetail + )) { + if (!empty($existingServices[$idHotelBookingDetail]['additional_services'])) { + $this->removeServicesFromOrderedRoom($existingServices[$idHotelBookingDetail]['additional_services']); + } + } + + // Populate wsRequestedRoomTypes with service data for consistent pricing + if (isset($room['services']) && $room['services']) { + $this->wsRequestedRoomTypes[$dateRoomJoinKey]['services'] = $room['services']; + } + $this->addServicesInOrderedRoom($room['services'], $idHotelBookingDetail); + $this->updateRoomOccupancyInOrder($idHotelBookingDetail, $room); + } + } else if (isset($roomsByDate['order']) + && $roomsByDate['order'] + ) { + $hotelBookingDetail = array_shift($roomsByDate['order']); + if ($existingServices = $objServiceProductOrderDetail->getRoomTypeServiceProducts( + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + null, + null, + null, + 0, + $hotelBookingDetail['id'] + )) { + if (!empty($existingServices[$hotelBookingDetail['id']]['additional_services'])) { + $this->removeServicesFromOrderedRoom($existingServices[$hotelBookingDetail['id']]['additional_services']); + } + } + + } + } + } + + // removing in the last. + if (count($roomsToRemove)) { + $this->removeRoomLineFromOrder($params, $roomsToRemove); + } + + // Cancel/refund without price_details re-prices nothing: keep the discounts. + if (!in_array($params['booking_status'], array(self::API_BOOKING_STATUS_CANCELLED, self::API_BOOKING_STATUS_REFUNDED)) + || isset($params['price_details']['total_price_with_tax']) + ) { + $cartRules = $objOrder->getCartRules(); + //Removing the stored cached object + $this->removeOrderCartRules($objOrder->id, $cartRules); + if (isset($params['cart_rules']) && $params['cart_rules']) { + $this->addCartRulesToOrder($params['cart_rules'], $objOrder->id); + } + } + + $this->updateRoomTaxRulesGroupsInOrder($objOrder->id); + + // Log failures here: the order state may not have changed. + try { + // Calling this after the older cart rules are removed from the order. + if ($params['booking_status'] != self::API_BOOKING_STATUS_CANCELLED && $params['booking_status'] != self::API_BOOKING_STATUS_REFUNDED) { + $this->manageOrderPrice($objOrder->id, $params); + $this->recalculateOrderFromComponents($objOrder->id); + } else { + $this->adjustPriceForOrderReversal($objOrder->id, $params); + } + + if (isset($params['booking_status'])) { + $objOrderState = false; + if ($params['booking_status'] == self::API_BOOKING_STATUS_CANCELLED) { + $objOrderState = new OrderState(Configuration::get('PS_OS_CANCELED')); + } elseif ($params['booking_status'] == self::API_BOOKING_STATUS_REFUNDED) { + $objOrderState = new OrderState(Configuration::get('PS_OS_REFUND')); + } elseif ($params['booking_status'] == self::API_BOOKING_STATUS_COMPLETED) { + $objOrderState = new OrderState(Configuration::get('PS_OS_PAYMENT_ACCEPTED')); + } elseif ($params['booking_status'] == self::API_BOOKING_STATUS_MODIFIED + && isset($params['payment_status']) + && $params['payment_status'] == self::API_BOOKING_PAYMENT_STATUS_COMPLETED + ) { + // The guest paid the OTA after booking (e.g. hotel collect turned prepaid). + $objOrderState = new OrderState(Configuration::get('PS_OS_PAYMENT_ACCEPTED')); + } elseif ($params['booking_status'] == self::API_BOOKING_STATUS_MODIFIED + && isset($params['payment_status']) + && $params['payment_status'] == self::API_BOOKING_PAYMENT_STATUS_PARTIAL + ) { + $sentPaidAmount = $this->getSentPaidAmount($params); + $objOrderState = new OrderState(Configuration::get( + ($sentPaidAmount !== null && isset($params['price_details']['total_price_with_tax']) + && $sentPaidAmount >= (float) $params['price_details']['total_price_with_tax']) + ? 'PS_OS_PAYMENT_ACCEPTED' + : 'PS_OS_PARTIAL_PAYMENT_ACCEPTED' + )); + } elseif ($params['booking_status'] == self::API_BOOKING_STATUS_NEW) { + $objOrderState = new OrderState(Configuration::get('PS_OS_AWAITING_PAYMENT')); + $paymentStatus = false; + if (isset($params['payment_status'])) { + $paymentStatus = $params['payment_status']; + } + + switch ($paymentStatus) { + case self::API_BOOKING_PAYMENT_STATUS_COMPLETED: + $objOrderState = new OrderState(Configuration::get('PS_OS_PAYMENT_ACCEPTED')); + break; + case self::API_BOOKING_PAYMENT_STATUS_PARTIAL: + $objOrderState = new OrderState(Configuration::get('PS_OS_PARTIAL_PAYMENT_ACCEPTED')); + break; + case self::API_BOOKING_PAYMENT_STATUS_AWATING: + $objOrderState = new OrderState(Configuration::get('PS_OS_AWAITING_PAYMENT')); + break; + default: + $objOrderState = new OrderState(Configuration::get('PS_OS_AWAITING_PAYMENT')); + break; + } + } + + if ($objOrderState) { + $statuses = array(Configuration::get('PS_OS_CANCELED'), Configuration::get('PS_OS_REFUND')); + if (!in_array($objOrder->current_state, $statuses) && in_array($objOrderState->id, $statuses)) { + if ($bookings = $objHotelBookingDetail->getOrderCurrentDataByOrderId($objOrder->id)) { + $objOrderReturn = new OrderReturn(); + $objOrderReturn->id_customer = $objOrder->id_customer; + $objOrderReturn->id_order = $objOrder->id; + $objOrderReturn->state = 0; + $objOrderReturn->by_admin = 1; + $objOrderReturn->refunded_amount = 0; + $objOrderReturn->save(); + if ($objOrderReturn->id) { + foreach ($bookings as $booking) { + $objHtlBooking = new HotelBookingDetail($booking['id']); + // No is_cancelled/is_refunded write (columns missing on fresh 1.8); + // the order return tracks the refunded rooms. + + $numDays = HotelHelper::getNumberOfDays($objHtlBooking->date_from, $objHtlBooking->date_to); + $objOrderReturnDetail = new OrderReturnDetail(); + $objOrderReturnDetail->id_order_return = $objOrderReturn->id; + $objOrderReturnDetail->id_order_detail = $objHtlBooking->id_order_detail; + $objOrderReturnDetail->product_quantity = $numDays; + $objOrderReturnDetail->id_htl_booking = $objHtlBooking->id; + $objOrderReturnDetail->refunded_amount = 0; + if (!$objOrder->getCartRules() && $objOrder->getTotalPaid() <= 0) { + $objOrderReturnDetail->id_customization = 1; + } + $objOrderReturnDetail->save(); + } + } + + try { + // A failed order_return state change must not block the order state change. + $objOrderReturn->changeIdOrderReturnState(Configuration::get('PS_ORS_REFUNDED')); + } catch (\Throwable $eReturnState) { + PrestaShopLogger::addLog( + 'qlocmconnector: could not update order_return state for order ' + .(int) $objOrder->id.' (order state flip continues): '.$eReturnState->getMessage(), + 2 + ); + } + + // Queue inventory ARI for all room types in the cancel request. + // The hook covers current (post-reallocation) DB room types; + // this covers original CM-side room types that may differ after reallocation. + if (!empty($params['room_bookings']) && is_array($params['room_bookings'])) { + $objAriUpdates = new QcmcAriUpdates(); + $objHotelRoomType = new HotelRoomType(); + foreach ($params['room_bookings'] as $roomBooking) { + if (!isset($roomBooking['id_room_type'])) { + continue; + } + $idRoomType = (int)$roomBooking['id_room_type']; + $roomTypeInfo = $objHotelRoomType->getRoomTypeInfoByIdProduct($idRoomType); + if (!$roomTypeInfo) { + continue; + } + $idHotel = (int)$roomTypeInfo['id_hotel']; + $dateFrom = date('Y-m-d', strtotime($roomBooking['check_in_date'])); + $dateTo = date('Y-m-d', strtotime($roomBooking['check_out_date'])); + $objAriUpdates->saveAriUpdateRow($idHotel, $idRoomType, $dateFrom, $dateTo); + } + } + } + } + $this->addOrderHistory($params['id_booking'], $objOrderState); + } + } + } catch (\Throwable $e) { + PrestaShopLogger::addLog( + 'qlocmconnector: cancel/refund price adjustment or state change failed for order ' + .(int) $objOrder->id.' - order state may not have been updated: '.$e->getMessage(), + 3 + ); + throw $e; + } + + if ( isset($params['price_details']['total_price_with_tax']) + && $objOrder->total_paid_real != $params['price_details']['total_price_with_tax'] + && isset($params['payment_status']) && (!in_array($params['payment_status'], array( + self::API_BOOKING_PAYMENT_STATUS_AWATING,self::API_BOOKING_STATUS_CANCELLED, self::API_BOOKING_STATUS_REFUNDED) + )) + ) { + $this->addOrderPayment($params); + } + $this->deleteWsFeaturePrices(); + QloCMConnector::$idBookingCm = null; + $this->getBookingDetails($objOrder->id); + } + + /** + * Finds order rooms that are segments of a split stay: consecutive bookings of one room + * type on different rooms that together cover one requested room's stay. + * + * @param array $orderRooms order rooms (htl_booking_detail rows) keyed by any key + * @param array $roomTypes requested room types from formatRoomTypesInRequestData() + * + * @return array order room key => ['room_type_key', 'chain', 'chain_nights', 'chain_segments'] + */ + protected function getSplitRoomSegments($orderRooms, $roomTypes) + { + $splitSegments = array(); + $chainIndex = 0; + foreach ($roomTypes as $roomTypeKey => $roomType) { + if (!isset($roomType['id_room_type'], $roomType['check_in_date'], $roomType['check_out_date'])) { + continue; + } + $requestedFrom = strtotime(date('Y-m-d', strtotime($roomType['check_in_date']))); + $requestedTo = strtotime(date('Y-m-d', strtotime($roomType['check_out_date']))); + + $segments = array(); + foreach ($orderRooms as $orderRoomKey => $orderRoom) { + if ((int) $orderRoom['id_product'] == (int) $roomType['id_room_type'] && !isset($splitSegments[$orderRoomKey])) { + $segments[$orderRoomKey] = array( + 'from' => strtotime(date('Y-m-d', strtotime($orderRoom['date_from']))), + 'to' => strtotime(date('Y-m-d', strtotime($orderRoom['date_to']))), + ); + } + } + + $used = array(); + foreach ($segments as $startKey => $startSegment) { + if ($startSegment['from'] != $requestedFrom || $startSegment['to'] >= $requestedTo || isset($used[$startKey])) { + continue; + } + $chain = array($startKey); + $chainEnd = $startSegment['to']; + while ($chainEnd < $requestedTo) { + $nextKey = false; + foreach ($segments as $segmentKey => $segment) { + if (!isset($used[$segmentKey]) && !in_array($segmentKey, $chain, true) && $segment['from'] == $chainEnd) { + $nextKey = $segmentKey; + break; + } + } + if ($nextKey === false) { + break; + } + $chain[] = $nextKey; + $chainEnd = $segments[$nextKey]['to']; + } + + if ($chainEnd == $requestedTo && count($chain) > 1) { + $chainIndex++; + foreach ($chain as $segmentKey) { + $used[$segmentKey] = true; + $splitSegments[$segmentKey] = array( + 'room_type_key' => $roomTypeKey, + 'chain' => $chainIndex, + 'chain_nights' => (int) round(($requestedTo - $requestedFrom) / 86400), + 'chain_segments' => count($chain), + ); + } + } + } + } + + return $splitSegments; + } + + /** + * Applies the sent occupancy (adults, children, ages) to the booking row and its cart row. + * + * @param int $idHotelBookingDetail + * @param array $room requested room data with an 'occupancy' entry + * + * @return bool + */ + public function updateRoomOccupancyInOrder($idHotelBookingDetail, $room) + { + if (!isset($room['occupancy']['adults'])) { + return false; + } + + $objHotelBookingDetail = new HotelBookingDetail((int) $idHotelBookingDetail); + if (!Validate::isLoadedObject($objHotelBookingDetail)) { + return false; + } + + $children = isset($room['occupancy']['children']) ? (int) $room['occupancy']['children'] : 0; + $childAges = array(); + if (isset($room['occupancy']['children_ages']) && is_array($room['occupancy']['children_ages'])) { + $childAges = $room['occupancy']['children_ages']; + } elseif (isset($room['occupancy']['child_ages']) && is_array($room['occupancy']['child_ages'])) { + $childAges = $room['occupancy']['child_ages']; + } + $childAges = array_slice(array_pad(array_map('intval', array_values($childAges)), $children, 17), 0, $children); + + $objHotelBookingDetail->adults = (int) $room['occupancy']['adults']; + $objHotelBookingDetail->children = $children; + $objHotelBookingDetail->child_ages = json_encode($childAges); + $result = $objHotelBookingDetail->save(); + + if ($idHotelCartBookingData = (int) Db::getInstance()->getValue( + 'SELECT `id` FROM `'._DB_PREFIX_.'htl_cart_booking_data` + WHERE `id_order` = '.(int) $objHotelBookingDetail->id_order.' + AND `id_room` = '.(int) $objHotelBookingDetail->id_room.' + AND `date_from` = "'.pSQL($objHotelBookingDetail->date_from).'" + AND `date_to` = "'.pSQL($objHotelBookingDetail->date_to).'"' + )) { + $objHotelCartBookingData = new HotelCartBookingData($idHotelCartBookingData); + $objHotelCartBookingData->adults = $objHotelBookingDetail->adults; + $objHotelCartBookingData->children = $objHotelBookingDetail->children; + $objHotelCartBookingData->child_ages = $objHotelBookingDetail->child_ages; + $result = $objHotelCartBookingData->save() && $result; + } + + return $result; + } + + /** + * Updating the room price. + */ + public function updateRoomPriceInOrder($room, $bookingData) + { + $idHotelBooking = $bookingData['id']; + if (Validate::isLoadedObject($objHotelBookingDetail = new HotelBookingDetail((int) $idHotelBooking))) { + $objOrder = new Order((int) $objHotelBookingDetail->id_order); + $objCart = new Cart($objOrder->id_cart); + $objOrderDetail = new OrderDetail((int) $objHotelBookingDetail->id_order_detail); + //removing the old price + $objOrder->total_paid -= $objHotelBookingDetail->total_price_tax_incl; + $objOrder->total_paid_tax_incl -= $objHotelBookingDetail->total_price_tax_incl; + $objOrder->total_paid_tax_excl -= $objHotelBookingDetail->total_price_tax_excl; + $objOrder->total_products -= $objHotelBookingDetail->total_price_tax_excl; + $objOrder->total_products_wt -= $objHotelBookingDetail->total_price_tax_incl; + + $objOrderDetail->total_price_tax_incl -= $objHotelBookingDetail->total_price_tax_incl; + $objOrderDetail->total_price_tax_excl -= $objHotelBookingDetail->total_price_tax_excl; + + $roomPriceTaxExcl = 0; + if (isset($room['unit_price_without_tax'])) { + $roomPriceTaxExcl = $room['unit_price_without_tax']; + } else if (isset($room['total_price_with_tax']) && isset($room['total_tax'])) { + $roomPriceTaxExcl = $room['total_price_with_tax'] - $room['total_tax']; + } + + if ($roomPriceTaxExcl) { + $featureDateFrom = isset($room['check_in_date']) ? $room['check_in_date'] : $room['date_from']; + $featureDateTo = isset($room['check_out_date']) ? $room['check_out_date'] : $room['date_to']; + $room['unit_price_without_tax'] = Tools::ps_round($roomPriceTaxExcl, _PS_PRICE_COMPUTE_PRECISION_); + $this->wsFeaturePrices[] = $this->createFeaturePrice( + array( + 'id_product' => (int) $objHotelBookingDetail->id_product, + 'id_cart' => (int) $objCart->id, + 'id_guest' => (int) $objCart->id_guest, + 'date_from' => $featureDateFrom, + 'date_to' => $featureDateTo, + 'id_room' => $objHotelBookingDetail->id_room, + 'price' => $room['unit_price_without_tax'] + ) + ); + } + + if (!isset($room['date_from']) && isset($room['check_in_date'])) { + $room['date_from'] = $room['check_in_date']; + } + if (!isset($room['date_to']) && isset($room['check_out_date'])) { + $room['date_to'] = $room['check_out_date']; + } + if (!isset($room['check_in_date']) && isset($room['date_from'])) { + $room['check_in_date'] = $room['date_from']; + } + if (!isset($room['check_out_date']) && isset($room['date_to'])) { + $room['check_out_date'] = $room['date_to']; + } + + $roomTotalPrice = HotelRoomTypeFeaturePricing::getRoomTypeTotalPrice( + $objHotelBookingDetail->id_product, + $room['date_from'], + $room['date_to'], + 0, + Group::getCurrent()->id, + $objCart->id, + $objCart->id_guest, + $objHotelBookingDetail->id_room, + 0 + ); + + $oldDateFrom = $objHotelBookingDetail->date_from; + $oldDateTo = $objHotelBookingDetail->date_to; + $oldNights = max((int) HotelHelper::getNumberOfDays($oldDateFrom, $oldDateTo), 1); + + $objHotelBookingDetail->date_from = $room['check_in_date']; + $objHotelBookingDetail->date_to = $room['check_out_date']; + + $objHotelBookingDetail->total_price_tax_incl = $roomTotalPrice['total_price_tax_excl'] + (isset($room['total_tax']) ? $room['total_tax'] : 0); + $objHotelBookingDetail->total_price_tax_excl = $roomTotalPrice['total_price_tax_excl']; + $objHotelBookingDetail->total_paid_amount = $objHotelBookingDetail->total_price_tax_incl; + $objHotelBookingDetail->save(); + + // Keep the cart booking row the room was ordered from on the same dates. + if (strtotime($oldDateFrom) != strtotime($objHotelBookingDetail->date_from) + || strtotime($oldDateTo) != strtotime($objHotelBookingDetail->date_to) + ) { + Db::getInstance()->update( + 'htl_cart_booking_data', + array( + 'date_from' => pSQL(date('Y-m-d H:i:s', strtotime($objHotelBookingDetail->date_from))), + 'date_to' => pSQL(date('Y-m-d H:i:s', strtotime($objHotelBookingDetail->date_to))), + ), + '`id_order` = '.(int) $objHotelBookingDetail->id_order.' + AND `id_room` = '.(int) $objHotelBookingDetail->id_room.' + AND `date_from` = "'.pSQL($oldDateFrom).'" + AND `date_to` = "'.pSQL($oldDateTo).'"' + ); + } + + // Updating the price + $objOrderDetail->total_price_tax_incl += $objHotelBookingDetail->total_price_tax_incl; + $objOrderDetail->total_price_tax_excl += $objHotelBookingDetail->total_price_tax_excl; + + // Sync quantity/unit price with the new night count (only this room's nights change). + $newNights = max((int) HotelHelper::getNumberOfDays($objHotelBookingDetail->date_from, $objHotelBookingDetail->date_to), 1); + $newQuantity = max((int) $objOrderDetail->product_quantity - $oldNights + $newNights, 1); + $objOrderDetail->product_quantity = $newQuantity; + $objOrderDetail->unit_price_tax_incl = Tools::ps_round($objOrderDetail->total_price_tax_incl / $newQuantity, _PS_PRICE_COMPUTE_PRECISION_); + $objOrderDetail->unit_price_tax_excl = Tools::ps_round($objOrderDetail->total_price_tax_excl / $newQuantity, _PS_PRICE_COMPUTE_PRECISION_); + + $objOrderDetail->save(); + + $objOrder->total_paid += $objHotelBookingDetail->total_price_tax_incl; + $objOrder->total_paid_tax_incl += $objHotelBookingDetail->total_price_tax_incl; + $objOrder->total_paid_tax_excl += $objHotelBookingDetail->total_price_tax_excl; + $objOrder->total_products += $objHotelBookingDetail->total_price_tax_excl; + $objOrder->total_products_wt += $objHotelBookingDetail->total_price_tax_incl; + $objOrder->update(); + + HotelRoomTypeFeaturePricing::deleteFeaturePrices($objCart->id); + } + } + + + /** + * Adding the service into the order. + */ + public function addServicesInOrderedRoom(array &$services, $idHotelBookingDetail) + { + if ($services) { + $objHotelBookingDetail = new HotelBookingDetail((int) $idHotelBookingDetail); + + $objOrder = new Order($objHotelBookingDetail->id_order); + $objAddress = new Address((int)$objOrder->id_address_tax); + + // set context currency So that we can get prices in the order currency + $this->context->currency = new Currency($objOrder->id_currency); + $objHotelCartBookingData = new HotelCartBookingData(); + $objRoomTypeServiceProductPrice = new RoomTypeServiceProductPrice(); + $objServiceProductCartDetail = new ServiceProductCartDetail(); + + $roomHtlCartInfo = $objHotelCartBookingData->getRoomRowByIdProductIdRoomInDateRange( + $objHotelBookingDetail->id_cart, + $objHotelBookingDetail->id_product, + $objHotelBookingDetail->date_from, + $objHotelBookingDetail->date_to, + $objHotelBookingDetail->id_room + ); + + $idTaxRulesGroup = 0; + if($idOrderDetail = $objHotelBookingDetail->id_order_detail) { + if(Validate::isLoadedObject($objOrderDetail = new OrderDetail((int)$idOrderDetail))){ + $idTaxRulesGroup = $objOrderDetail->id_tax_rules_group; + } + } + + $this->createCartForOrder($objOrder->id); + $idOrderInvoice = false; + if (($objOrderInvoice = $objOrder->getInvoicesCollection()->getFirst()) + && Validate::isLoadedObject($objOrderInvoice) + ) { + $idOrderInvoice = $objOrderInvoice->id; + } + + $objCart = $this->context->cart; + $invoiceDeltaProductsExcl = 0; + $invoiceDeltaProductsIncl = 0; + $invoiceDeltaPaidExcl = 0; + $invoiceDeltaPaidIncl = 0; + $formattedServices = array(); + foreach ($services as $serviceKey => &$service) { + if (!isset($service['id_product'])) { + $idServiceTaxRulesGroup = 0; + $servicePriceTaxExculded = floatval($service['total_price_with_tax']) - floatval($service['total_tax']); + $serviceTaxRate = $servicePriceTaxExculded > 0 ? (floatval($service['total_tax'])/floatval($servicePriceTaxExculded))*100 : 0; + if(isset($service['taxes']) && $service['taxes']) { + $idServiceTaxRulesGroup = $this->createTaxRulesGroup($serviceTaxRate, $service['taxes'], $objAddress); + } else { + $idServiceTaxRulesGroup = $this->createTaxRulesGroup($serviceTaxRate, false, $objAddress); + } + $service['id_tax_rules_group'] = $idServiceTaxRulesGroup; + + if (!isset($service['quantity'])) { + $service['quantity'] = 1; + } + if (isset($service['price_mode']) && $service['price_mode'] == self::SERVICE_PRICE_MODE_PER_PERSON_PER_NIGHT) { + // Fold per-person into quantity; nights come from the per-night calculation method. + $numOccupants = max((int) $objHotelBookingDetail->adults + (int) $objHotelBookingDetail->children, 1); + $service['quantity'] = $service['quantity'] * $numOccupants; + } + + $serviceKey = $this->createWsService($service, $objHotelBookingDetail->id_product, $idServiceTaxRulesGroup); + $service['id_product'] = $serviceKey; + } + + if (!isset($service['quantity'])) { + $service['quantity'] = 1; + } + $formattedServices[$serviceKey] = $service; + } + unset($service); + + $services = $formattedServices; + foreach ($services as $service) { + $objServiceProductCartDetail->addServiceProductInCart( + $objCart->id, + $service['id_product'], + $service['quantity'], + false, + isset($roomHtlCartInfo['id']) ? $roomHtlCartInfo['id'] : false + ); + } + + $unitPriceTaxIncl = 0; + $unitPriceTaxExcl = 0; + $productList = $objCart->getProducts(true); + $objOrderDetail = new OrderDetail(); + + $objOrderDetail->createList($objOrder, $objCart, $objOrder->getCurrentOrderState(), $productList, $idOrderInvoice, true); + foreach ($productList as &$product) { + if ($idServiceProductCartDetail = $objServiceProductCartDetail->alreadyExists( + $objCart->id, + $product['id_product'], + isset($roomHtlCartInfo['id']) ? $roomHtlCartInfo['id'] : false + ) + ) { + $objServiceProductCartDetail = new ServiceProductCartDetail((int) $idServiceProductCartDetail); + $insertedServiceProductIdOrderDetail = $objHotelBookingDetail->getLastInsertedServiceIdOrderDetail($objOrder->id, $product['id_product']); + $objOrderDetail = new OrderDetail($insertedServiceProductIdOrderDetail); + + $numDays = self::getServiceBillableDays( + Product::getProductPriceCalculation($product['id_product']), + $objHotelBookingDetail->date_from, + $objHotelBookingDetail->date_to + ); + + $quantity = $objServiceProductCartDetail->quantity * $numDays; + + $unitPriceTaxExcl = Product::getServiceProductPrice( + (int) $product['id_product'], + 0, + false, + false, + false, + 1, + $objHotelBookingDetail->date_from, + $objHotelBookingDetail->date_to, + $objCart->id + )/ $numDays; + + $unitPriceTaxIncl = Product::getServiceProductPrice( + (int) $product['id_product'], + 0, + false, + false, + true, + 1, + $objHotelBookingDetail->date_from, + $objHotelBookingDetail->date_to, + $objCart->id + )/ $numDays; + + + if ($unitPriceTaxIncl > 0) { + $oldTaxMultiplier = $unitPriceTaxExcl / $unitPriceTaxIncl; + } else { + $oldTaxMultiplier = 1; + } + + $totalPriceTaxExcl = $unitPriceTaxExcl * $quantity; + $totalPriceTaxIncl = $unitPriceTaxIncl * $quantity; + + $objOrderDetail->unit_price_tax_incl -= $unitPriceTaxIncl; + $objOrderDetail->unit_price_tax_excl -= $unitPriceTaxExcl; + $objOrderDetail->total_price_tax_excl -= $totalPriceTaxExcl; + $objOrderDetail->total_price_tax_incl -= $totalPriceTaxIncl; + + if (isset($services[$product['id_product']]['unit_price_without_tax'])) { + $unitPriceTaxExcl = $services[$product['id_product']]['unit_price_without_tax']; + $unitPriceTaxIncl = $unitPriceTaxExcl * $oldTaxMultiplier; + + $totalPriceTaxExcl = $unitPriceTaxExcl * $quantity; + $totalPriceTaxIncl = $unitPriceTaxIncl * $quantity; + } elseif (isset($services[$product['id_product']]['total_price_without_tax'])) { + $totalPriceTaxExcl = $services[$product['id_product']]['total_price_without_tax']; + $totalPriceTaxIncl = $totalPriceTaxExcl * $oldTaxMultiplier; + + $unitPriceTaxExcl = $quantity > 0 ? $totalPriceTaxExcl / $quantity : 0; + $unitPriceTaxIncl = $quantity > 0 ? $totalPriceTaxIncl / $quantity : 0; + } + + if (isset($services[$product['id_product']]['id_tax_rules_group'])) { + if (isset($services[$product['id_product']]['total_tax'])) { + // Use the sent total_tax, not a recomputed rate; the group is kept for tax rows. + $totalPriceTaxIncl = $totalPriceTaxExcl + $services[$product['id_product']]['total_tax']; + $totalPriceTaxIncl = Tools::ps_round(($totalPriceTaxIncl), _PS_PRICE_COMPUTE_PRECISION_); + $unitPriceTaxIncl = Tools::ps_round($quantity > 0 ? ($totalPriceTaxIncl / $quantity) : 0, _PS_PRICE_COMPUTE_PRECISION_); + } else { + $objAddress = new Address((int) $objOrder->id_address_tax); + $objTaxManager = TaxManagerFactory::getManager($objAddress, $services[$product['id_product']]['id_tax_rules_group']); + $objTaxCalculator = $objTaxManager->getTaxCalculator(); + $unitPriceTaxIncl = $objTaxCalculator->addTaxes($unitPriceTaxExcl); + + $unitPriceTaxIncl = Tools::ps_round($unitPriceTaxIncl, _PS_PRICE_COMPUTE_PRECISION_); + $totalPriceTaxIncl = Tools::ps_round(($unitPriceTaxIncl * $quantity), _PS_PRICE_COMPUTE_PRECISION_); + } + } elseif (isset($services[$product['id_product']]['total_tax'])) { + $totalPriceTaxIncl = $totalPriceTaxExcl + $services[$product['id_product']]['total_tax']; + $totalPriceTaxIncl = Tools::ps_round(($totalPriceTaxIncl), _PS_PRICE_COMPUTE_PRECISION_); + $unitPriceTaxIncl = Tools::ps_round($quantity > 0 ? ($totalPriceTaxIncl / $quantity) : 0, _PS_PRICE_COMPUTE_PRECISION_); + } + + $objServiceProductOrderDetail = new ServiceProductOrderDetail(); + $objServiceProductOrderDetail->id_product = $product['id_product']; + $objServiceProductOrderDetail->id_order = $objHotelBookingDetail->id_order; + $objServiceProductOrderDetail->id_order_detail = $objOrderDetail->id; + $objServiceProductOrderDetail->id_cart = $objCart->id; + $objServiceProductOrderDetail->id_htl_booking_detail = $objHotelBookingDetail->id; + $objServiceProductOrderDetail->unit_price_tax_excl = Tools::ps_round($unitPriceTaxExcl, _PS_PRICE_COMPUTE_PRECISION_); + $objServiceProductOrderDetail->unit_price_tax_incl = Tools::ps_round($unitPriceTaxIncl, _PS_PRICE_COMPUTE_PRECISION_); + $objServiceProductOrderDetail->total_price_tax_excl = Tools::ps_round($totalPriceTaxExcl, _PS_PRICE_COMPUTE_PRECISION_); + $objServiceProductOrderDetail->total_price_tax_incl = Tools::ps_round($totalPriceTaxIncl, _PS_PRICE_COMPUTE_PRECISION_); + $objServiceProductOrderDetail->name = $product['name']; + $objServiceProductOrderDetail->quantity = $objServiceProductCartDetail->quantity; + $objServiceProductOrderDetail->save(); + + // update totals amount of order + $objOrder->total_products += $objServiceProductOrderDetail->total_price_tax_excl; + $objOrder->total_products_wt += $objServiceProductOrderDetail->total_price_tax_incl; + + $objOrder->total_paid += Tools::ps_round($objServiceProductOrderDetail->total_price_tax_incl, _PS_PRICE_COMPUTE_PRECISION_); + $objOrder->total_paid_tax_excl += Tools::ps_round($objServiceProductOrderDetail->total_price_tax_excl, _PS_PRICE_COMPUTE_PRECISION_); + $objOrder->total_paid_tax_incl += Tools::ps_round($objServiceProductOrderDetail->total_price_tax_incl, _PS_PRICE_COMPUTE_PRECISION_); + + $invoiceDeltaProductsExcl += $objServiceProductOrderDetail->total_price_tax_excl; + $invoiceDeltaProductsIncl += $objServiceProductOrderDetail->total_price_tax_incl; + $invoiceDeltaPaidExcl += Tools::ps_round($objServiceProductOrderDetail->total_price_tax_excl, _PS_PRICE_COMPUTE_PRECISION_); + $invoiceDeltaPaidIncl += Tools::ps_round($objServiceProductOrderDetail->total_price_tax_incl, _PS_PRICE_COMPUTE_PRECISION_); + + $objOrderDetail->unit_price_tax_excl += $unitPriceTaxExcl; + $objOrderDetail->unit_price_tax_incl += $unitPriceTaxIncl; + $objOrderDetail->total_price_tax_excl += $unitPriceTaxExcl * $objServiceProductOrderDetail->quantity; + $objOrderDetail->total_price_tax_incl += $unitPriceTaxIncl * $objServiceProductOrderDetail->quantity; + } + } + + $objOrderDetail->update(); + + $objOrder->total_discounts += (float)abs($objCart->getOrderTotal(true, Cart::ONLY_DISCOUNTS)); + $objOrder->total_discounts_tax_excl += (float)abs($objCart->getOrderTotal(false, Cart::ONLY_DISCOUNTS)); + $objOrder->total_discounts_tax_incl += (float)abs($objCart->getOrderTotal(true, Cart::ONLY_DISCOUNTS)); + $objOrder->update(); + + // Sync invoice totals with the added service lines. + if ($idOrderInvoice) { + $objOrderInvoice = new OrderInvoice((int) $idOrderInvoice); + $objOrderInvoice->total_products += $invoiceDeltaProductsExcl; + $objOrderInvoice->total_products_wt += $invoiceDeltaProductsIncl; + $objOrderInvoice->total_paid_tax_excl += $invoiceDeltaPaidExcl; + $objOrderInvoice->total_paid_tax_incl += $invoiceDeltaPaidIncl; + $objOrderInvoice->update(); + } + } + } + + /** + * Since we have to create new cart to add rooms and services in the order. + * So, we are creating new cart. + */ + public function createCartForOrder($idOrder) + { + $objOrder = new Order((int) $idOrder); + $objCart = new Cart(); + $objCart->id_shop_group = $objOrder->id_shop_group; + $objCart->id_shop = $objOrder->id_shop; + $objCart->id_customer = $objOrder->id_customer; + $objCart->id_carrier = $objOrder->id_carrier; + $objCart->id_address_delivery = $objOrder->id_address_delivery; + $objCart->id_address_invoice = $objOrder->id_address_invoice; + $objCart->id_currency = $objOrder->id_currency; + $objCart->id_lang = $objOrder->id_lang; + $objCart->secure_key = $objOrder->secure_key; + $objCart->id_guest = Guest::getFromCustomer((int) $objOrder->id_customer); + // Save new cart + $objCart->add(); + + // Save context (in order to apply cart rule) + $this->context->cart = $objCart; + $this->context->customer = new Customer((int) $objOrder->id_customer); + } + + /** + * Removing room from the order. + */ + public function removeRoomLineFromOrder($params, $roomsToRemove) + { + $objOrder = new Order((int) $params['id_booking']); + $objHotelBookingDetail = new HotelBookingDetail(); + $objServiceProductOrderDetail = new ServiceProductOrderDetail(); + foreach ($roomsToRemove as $roomType) { + $dateFrom = $roomType['date_from']; + $dateTo = $roomType['date_to']; + $quantity = (int) HotelHelper::getNumberOfDays($dateFrom, $dateTo); + $objOrderDetail = new OrderDetail((int) $roomType['id_order_detail']); + $idHotelBooking = $roomType['id']; + $idHotel = $roomType['id_hotel']; + $bookingPriceTaxIncl = $roomType['total_price_tax_incl']; + $bookingPriceTaxExcl = $roomType['total_price_tax_excl']; + $additionlServicesTI = $objServiceProductOrderDetail->getRoomTypeServiceProducts( + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 1, + 1, + null, + null, + 0, + $idHotelBooking + ); + $additionlServicesTE = $objServiceProductOrderDetail->getRoomTypeServiceProducts( + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 1, + 0, + null, + null, + 0, + $idHotelBooking + ); + + $diffProductsTaxIncl = $bookingPriceTaxIncl; + $diffProductsTaxExcl = $bookingPriceTaxExcl; + $objHotelBookingDetail = new HotelBookingDetail((int) $idHotelBooking); + $roomQuantity = (int) HotelHelper::getNumberOfDays($dateFrom, $dateTo); + if ($selectedAdditonalServices = $objServiceProductOrderDetail->getRoomTypeServiceProducts( + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + null, + null, + null, + 0, + $idHotelBooking + )) { + if (isset($selectedAdditonalServices[$idHotelBooking]['additional_services']) + && count($selectedAdditonalServices[$idHotelBooking]['additional_services']) + ) { + foreach ($selectedAdditonalServices[$idHotelBooking]['additional_services'] as $service) { + $serviceOrderDetail = new OrderDetail($service['id_order_detail']); + $cart_quantity = $service['quantity']; + $cart_quantity = $cart_quantity * self::getServiceBillableDays($service['price_calculation_method'], $dateFrom, $dateTo); + + if ($cart_quantity >= $serviceOrderDetail->product_quantity) { + $serviceOrderDetail->delete(); + } else { + $serviceOrderDetail->total_price_tax_incl -= Tools::ps_round($service['total_price_tax_incl'], _PS_PRICE_COMPUTE_PRECISION_); + $serviceOrderDetail->total_price_tax_excl -= Tools::ps_round($service['total_price_tax_excl'], _PS_PRICE_COMPUTE_PRECISION_); + $serviceOrderDetail->product_quantity -= $cart_quantity; + + // update taxes + $serviceOrderDetail->updateTaxAmount($objOrder); + + // Save order detail + $serviceOrderDetail->update(); + } + } + } + } + + // Update Order + // values changes as values are calculated accoding to the quantity of the product by webkul + $objOrder->total_paid = Tools::ps_round($objOrder->total_paid - ($diffProductsTaxIncl + $additionlServicesTI), _PS_PRICE_COMPUTE_PRECISION_); + $objOrder->total_paid_tax_incl = Tools::ps_round($objOrder->total_paid_tax_incl - ($diffProductsTaxIncl + $additionlServicesTI), _PS_PRICE_COMPUTE_PRECISION_); + $objOrder->total_paid_tax_excl = Tools::ps_round($objOrder->total_paid_tax_excl - ($diffProductsTaxExcl + $additionlServicesTE), _PS_PRICE_COMPUTE_PRECISION_); + $objOrder->total_products = Tools::ps_round($objOrder->total_products - ($diffProductsTaxExcl + $additionlServicesTE), _PS_PRICE_COMPUTE_PRECISION_); + $objOrder->total_products_wt = Tools::ps_round($objOrder->total_products_wt - ($diffProductsTaxIncl + $additionlServicesTI), _PS_PRICE_COMPUTE_PRECISION_); + + $objBookingDetail = new HotelBookingDetail($idHotelBooking); + $bookingPriceTaxIncl = $objBookingDetail->total_price_tax_incl; + $bookingPriceTaxExcl = $objBookingDetail->total_price_tax_excl; + + if ($objOrderDetail->id_order_invoice != 0) { + $objOrderInvoice = new OrderInvoice($objOrderDetail->id_order_invoice); + $objOrderInvoice->total_paid_tax_excl = $objOrder->total_paid_tax_excl; + $objOrderInvoice->total_paid_tax_incl = $objOrder->total_paid_tax_incl; + $objOrderInvoice->total_products = $objOrder->total_products; + $objOrderInvoice->total_products_wt = $objOrder->total_products_wt; + $objOrderInvoice->update(); + } + + if ($roomQuantity >= $objOrderDetail->product_quantity) { + $objOrderDetail->delete(); + } else { + $objOrderDetail->total_price_tax_incl -= $diffProductsTaxIncl; + $objOrderDetail->total_price_tax_excl -= $bookingPriceTaxExcl; + $oldRoomQuantity = $objOrderDetail->product_quantity; + $objOrderDetail->product_quantity = $oldRoomQuantity - $roomQuantity; + $objOrderDetail->reduction_percent = 0; + // update taxes + $objOrderDetail->updateTaxAmount($objOrder); + // Save order detail + $objOrderDetail->update(); + } + if(!Validate::isPrice($objOrder->total_paid)) { + $objOrder->total_paid = 0; + } + $objOrder->update(); + $objServiceProductOrderDetail->deleteSeviceProducts(0, $idHotelBooking); + $objHotelCartBookingData = new HotelCartBookingData(); + $objHotelCartBookingData->deleteOrderedRoomFromCart( + $objOrder->id, + $idHotel, + $roomType['id_room'], + $dateFrom, + $dateTo + ); + $objHotelBookingDetail = new HotelBookingDetail(); + $objHotelBookingDetail->deleteOrderedRoomFromOrder( + $objOrder->id, + $idHotel, + $roomType['id_room'], + $dateFrom, + $dateTo + ); + $objCart = new Cart($roomType['id_cart']); + $objCart->updateQty($quantity, $roomType['id_product'], null, false, 'down', 0, null, true); + } + } + + + /** + * Adding the cart rules in to the order. + */ + public function addCartRulesToOrder($cartRules, $idOrder) + { + $objOrder = new Order((int) $idOrder); + $cartRulesFormatted = array(); + foreach ($cartRules as $key => $cartRule) { + $cartRulesFormatted[$key]['code'] = $cartRule['code']; + $cartRulesFormatted[$key]['value'] = $cartRule['value']; + $cartRulesFormatted[$key]['type'] = $cartRule['type']; + $cartRulesFormatted[$key]['currency'] = isset($cartRule['currency']) ? $cartRule['currency'] : ''; + } + + // Tax rate from the order's corrected totals; getTaxesAverageUsed() uses the catalog rate. + $taxRate = $objOrder->total_paid_tax_excl > 0 + ? (($objOrder->total_paid_tax_incl - $objOrder->total_paid_tax_excl) / $objOrder->total_paid_tax_excl) + : 0; + + if ($idCartRules = $this->createCartRules($cartRulesFormatted)) { + foreach ($idCartRules as $idCartRule) { + $objCartRule = new CartRule($idCartRule); + $invoiceCartRules = array(); + if (($invoiceCollection = $objOrder->getInvoicesCollection()) && count($invoiceCollection)) { + foreach ($invoiceCollection as $orderInvoice) { + if ((int) $objCartRule->reduction_amount) { + if (!($objCartRule->reduction_amount > $orderInvoice->total_paid_tax_incl)) { + $this->error_msg = Tools::displayError('The discount value is greater than the order invoice total.').$orderInvoice->getInvoiceNumberFormatted(Context::getContext()->language->id, (int)$objOrder->id_shop).')'; + $invoiceCartRules[$orderInvoice->id]['value_tax_incl'] = Tools::ps_round($objCartRule->reduction_amount, _PS_PRICE_COMPUTE_PRECISION_); + $invoiceCartRules[$orderInvoice->id]['value_tax_excl'] = Tools::ps_round($objCartRule->reduction_amount / (1 + $taxRate), _PS_PRICE_COMPUTE_PRECISION_); + + $this->applyDiscountOnInvoice($orderInvoice, $invoiceCartRules[$orderInvoice->id]['value_tax_incl'], $invoiceCartRules[$orderInvoice->id]['value_tax_excl']); + } + } elseif ((float) $objCartRule->reduction_percent) { + $invoiceCartRules[$orderInvoice->id]['value_tax_incl'] = Tools::ps_round($objOrder->total_paid_tax_incl * $objCartRule->reduction_percent / 100, _PS_PRICE_COMPUTE_PRECISION_); + $invoiceCartRules[$orderInvoice->id]['value_tax_excl'] = Tools::ps_round($objOrder->total_paid_tax_excl * $objCartRule->reduction_percent / 100, _PS_PRICE_COMPUTE_PRECISION_); + + $this->applyDiscountOnInvoice($orderInvoice, $invoiceCartRules[$orderInvoice->id]['value_tax_incl'], $invoiceCartRules[$orderInvoice->id]['value_tax_excl']); + } + } + } else { + // Cast: DB values like "0.00" are truthy strings. + if ((float) $objCartRule->reduction_percent) { + $invoiceCartRules[0]['value_tax_incl'] = Tools::ps_round($objOrder->total_paid_tax_incl * $objCartRule->reduction_percent / 100, _PS_PRICE_COMPUTE_PRECISION_); + $invoiceCartRules[0]['value_tax_excl'] = Tools::ps_round($objOrder->total_paid_tax_excl * $objCartRule->reduction_percent / 100, _PS_PRICE_COMPUTE_PRECISION_); + } elseif ((float) $objCartRule->reduction_amount) { + $invoiceCartRules[0]['value_tax_incl'] = Tools::ps_round($objCartRule->reduction_amount, _PS_PRICE_COMPUTE_PRECISION_); + $invoiceCartRules[0]['value_tax_excl'] = Tools::ps_round($objCartRule->reduction_amount / (1 + $taxRate), _PS_PRICE_COMPUTE_PRECISION_); + } + } + + // Create OrderCartRule + foreach ($invoiceCartRules as $idInvoice => $rule) { + $ObjOrderCartRule = new OrderCartRule(); + $ObjOrderCartRule->id_order = $objOrder->id; + $ObjOrderCartRule->id_cart_rule = $objCartRule->id; + $ObjOrderCartRule->id_order_invoice = $idInvoice; + $ObjOrderCartRule->name = $objCartRule->code; + $ObjOrderCartRule->value = $rule['value_tax_incl']; + $ObjOrderCartRule->value_tax_excl = $rule['value_tax_excl']; + $ObjOrderCartRule->free_shipping = 0; + $ObjOrderCartRule->add(); + + $objOrder->total_discounts = Tools::ps_round($objOrder->total_discounts + $ObjOrderCartRule->value, _PS_PRICE_COMPUTE_PRECISION_); + $objOrder->total_discounts_tax_incl = Tools::ps_round($objOrder->total_discounts_tax_incl + $ObjOrderCartRule->value, _PS_PRICE_COMPUTE_PRECISION_); + $objOrder->total_discounts_tax_excl = Tools::ps_round($objOrder->total_discounts_tax_excl + $ObjOrderCartRule->value_tax_excl, _PS_PRICE_COMPUTE_PRECISION_); + $objOrder->total_paid = Tools::ps_round($objOrder->total_paid - $ObjOrderCartRule->value, _PS_PRICE_COMPUTE_PRECISION_); + $objOrder->total_paid_tax_incl = Tools::ps_round($objOrder->total_paid_tax_incl - $ObjOrderCartRule->value, _PS_PRICE_COMPUTE_PRECISION_); + $objOrder->total_paid_tax_excl = Tools::ps_round($objOrder->total_paid_tax_excl - $ObjOrderCartRule->value_tax_excl, _PS_PRICE_COMPUTE_PRECISION_); + } + } + } + + $objOrder->update(); + } + + /** + * Removing the cart rules from the order. + */ + public function removeOrderCartRules($idOrder, $cartRules = array()) + { + if ($cartRules) { + $objOrder = new Order($idOrder); + foreach ($cartRules as $rule) { + $objOrderCartRule = new OrderCartRule($rule['id_order_cart_rule']); + if ($objOrderCartRule->id_order_invoice) { + $objOrderInvoice = new OrderInvoice($objOrderCartRule->id_order_invoice); + $objOrderInvoice->total_discount_tax_excl = ($objOrderInvoice->total_discount_tax_excl - $objOrderCartRule->value_tax_excl) > 0 ? ($objOrderInvoice->total_discount_tax_excl - $objOrderCartRule->value_tax_excl) : 0; + $objOrderInvoice->total_discount_tax_incl = ($objOrderInvoice->total_discount_tax_incl - $objOrderCartRule->value) > 0 ? ($objOrderInvoice->total_discount_tax_incl - $objOrderCartRule->value) : 0; + + $objOrderInvoice->total_paid_tax_excl += $objOrderCartRule->value_tax_excl; + $objOrderInvoice->total_paid_tax_incl += $objOrderCartRule->value; + + // Update Order Invoice + $objOrderInvoice->update(); + } + + $objOrder->total_discounts = ($objOrder->total_discounts - $objOrderCartRule->value) > 0 ? ($objOrder->total_discounts - $objOrderCartRule->value) : 0; + $objOrder->total_discounts_tax_incl = ($objOrder->total_discounts_tax_incl - $objOrderCartRule->value) > 0 ? ($objOrder->total_discounts_tax_incl - $objOrderCartRule->value) : 0; + $objOrder->total_discounts_tax_excl = ($objOrder->total_discounts_tax_excl - $objOrderCartRule->value_tax_excl) > 0 ? ($objOrder->total_discounts_tax_excl - $objOrderCartRule->value_tax_excl) : 0; + + $objOrder->total_paid += $objOrderCartRule->value; + $objOrder->total_paid_tax_incl += $objOrderCartRule->value; + $objOrder->total_paid_tax_excl += $objOrderCartRule->value_tax_excl; + + // Delete Order Cart Rule and update Order + $objOrderCartRule->delete(); + } + + $objOrder->update(); + } + } + + /** + * Adding rooms in the cart. + */ + public function addRoomsInCart(array &$roomTypes, $idAddressTax = false) + { + $objRoomType = new HotelRoomType(); + $objHotelCartBookingData = new HotelCartBookingData(); + $roomUnitSelectionType = Configuration::get('PS_FRONT_ROOM_UNIT_SELECTION_TYPE'); + + if($idAddressTax) { + $objAddress = new Address((int)$idAddressTax); + } else if (isset($this->id_hotel) && $this->id_hotel) { + if(Validate::isLoadedObject($objHotelBranchInformation = new HotelBranchInformation((int)$this->id_hotel))) { + $objAddress = new Address((int)$objHotelBranchInformation->getHotelIdAddress()); + } + } + $objBookingDetail = new HotelBookingDetail(); + + $quantityWise = false; + if ($roomUnitSelectionType != HotelBookingDetail::PS_ROOM_UNIT_SELECTION_TYPE_OCCUPANCY) { + $quantityWise = true; + } + + foreach ($roomTypes as &$roomType) { + $dateFrom = date('Y-m-d', strtotime($roomType['check_in_date'])); + $dateTo = date('Y-m-d', strtotime($roomType['check_out_date'])); + $roomTypeInfo = $objRoomType->getRoomTypeInfoByIdProduct($roomType['id_room_type']); + $idHotel = $roomTypeInfo['id_hotel']; + $occupancy = array( + array( + 'adults' => $roomTypeInfo['adults'], + 'children' => $roomTypeInfo['children'], + 'child_ages' => array() + ) + ); + + if (isset($roomType['rooms']) && count($roomType['rooms'])) { + foreach ($roomType['rooms'] as &$room) { + $roomServices = array(); + $totalPriceTaxExcl = $room['total_price_with_tax'] - $room['total_tax']; + $roomTaxRate = $totalPriceTaxExcl > 0 ? ($room['total_tax']/$totalPriceTaxExcl)*100 : 0; + if(isset($room['taxes']) && $room['taxes']) { + $room['id_tax_rules_group'] = $this->createTaxRulesGroup($roomTaxRate, $room['taxes'], $objAddress); + } else { + $room['id_tax_rules_group'] = $this->createTaxRulesGroup($roomTaxRate, false, $objAddress); + } + + if (isset($room['services']) && $room['services']) { + $idServiceTaxRulesGroup = 0; + foreach ($room['services'] as $serviceKey => &$service) { + $servicePriceTaxExculded = floatval($service['total_price_with_tax']) - floatval($service['total_tax']); + $serviceTaxRate = $servicePriceTaxExculded > 0 ? (floatval($service['total_tax'])/floatval($servicePriceTaxExculded))*100 : 0; + if(isset($service['taxes']) && $service['taxes']) { + $idServiceTaxRulesGroup = $this->createTaxRulesGroup($serviceTaxRate, $service['taxes'], $objAddress); + } else { + $idServiceTaxRulesGroup = $this->createTaxRulesGroup($serviceTaxRate, false, $objAddress); + } + $service['id_tax_rules_group'] = $idServiceTaxRulesGroup; + + if (isset($service['price_mode']) && $service['price_mode'] == self::SERVICE_PRICE_MODE_PER_PERSON_PER_NIGHT) { + // Fold per-person into quantity; nights come from the per-night calculation method. + if (!isset($service['quantity'])) { + $service['quantity'] = 1; + } + $numOccupants = max( + (int) (isset($room['occupancy']['adults']) ? $room['occupancy']['adults'] : 0) + + (int) (isset($room['occupancy']['children']) ? $room['occupancy']['children'] : 0), + 1 + ); + $service['quantity'] = $service['quantity'] * $numOccupants; + } + + $service['id_product'] = $this->createWsService($service, $roomType['id_room_type'], $idServiceTaxRulesGroup); + + $serviceKey = $service['id_product']; + if (!isset($service['quantity'])) { + $service['quantity'] = 1; + } + $roomServices[$serviceKey] = $service; + } + unset($service); + } + + $idRoom = 0; + if (isset($room['id_room'])) { + $idRoom = $room['id_room']; + } + + $child_ages = array(); + if(isset($room['occupancy']['children_ages'])) { + $child_ages = $room['occupancy']['children_ages']; + } else if(isset($room['occupancy']['child_ages'])) { + $child_ages = $room['occupancy']['child_ages']; + } + + // add default age if the age if any of the missing + $child_ages = array_pad($child_ages, isset($room['occupancy']['children']) ? $room['occupancy']['children'] : 0, 17); + + $occupancy = array( + array( + 'adults' => $room['occupancy']['adults'], + 'children' => $room['occupancy']['children'], + 'child_ages' => $child_ages + ) + ); + + if ($quantityWise) { + $occupancy = 1; // since we are adding rooms one by one into the cart. + } + + //updated code + $bookingParams = array( + 'date_from' => $dateFrom, + 'date_to' => $dateTo, + 'hotel_id' => $idHotel, + 'id_room_type' => $roomType['id_room_type'], + 'only_search_data' => 1 + ); + + if (($hotelRoomData = $objBookingDetail->dataForFrontSearch($bookingParams)) + && isset($hotelRoomData['rm_data'][$roomType['id_room_type']]['data']['available']) + && $hotelRoomData['rm_data'][$roomType['id_room_type']]['data']['available'] + ) { + if (!($idHtlCartBookingData = $objHotelCartBookingData->updateCartBooking( + $roomType['id_room_type'], + $occupancy, + 'up', + $idHotel, + $idRoom, + $dateFrom, + $dateTo, + $roomServices, + $this->context->cart->id, + $this->context->cart->id_guest + ))) { + // Don't silently book fewer rooms than the OTA sold. + $this->error_msg = Tools::displayError('Requested room(s) not available'); + return false; + } else { + $objCartBookingData = new HotelCartBookingData((int) $idHtlCartBookingData); + $dateRoomJoinKey = strtotime($dateFrom).strtotime($dateTo).$objCartBookingData->id_product.$objCartBookingData->id_room; + // To update the price after valiate order is called. + $this->wsRequestedRoomTypes[$dateRoomJoinKey]['services'] = $roomServices; + $this->wsRequestedRoomTypes[$dateRoomJoinKey]['room'] = $room; + + if (isset($room['id_tax_rules_group']) && Validate::isLoadedObject(new TaxRulesGroup((int) $room['id_tax_rules_group']))) { + // Keep both: the group for tax rows, the sent total_tax for the price. + $this->wsRequestedRooms[$dateRoomJoinKey]['id_tax_rules_group'] = $room['id_tax_rules_group']; + } + if (isset($room['total_tax'])) { + $this->wsRequestedRooms[$dateRoomJoinKey]['total_tax'] = $room['total_tax']; + } + + + $roomPriceTaxExcl = 0; + if (isset($room['unit_price_without_tax'])) { + $roomPriceTaxExcl = $room['unit_price_without_tax']; + } else if (isset($room['total_price_with_tax']) && isset($room['total_tax'])) { + $roomPriceTaxExcl = $room['total_price_with_tax'] - $room['total_tax']; + } + + $room['unit_price_without_tax'] = Tools::ps_round($roomPriceTaxExcl, _PS_PRICE_COMPUTE_PRECISION_); + $this->wsFeaturePrices[] = $this->createFeaturePrice( + array( + 'id_product' => (int) $roomType['id_room_type'], + 'id_cart' => (int) $this->context->cart->id, + 'id_guest' => (int) $this->context->cart->id_guest, + 'date_from' => date('Y-m-d', strtotime($dateFrom)), + 'date_to' => date('Y-m-d', strtotime($dateTo)), + 'id_room' => $objCartBookingData->id_room, + 'price' => $room['unit_price_without_tax'] + ) + ); + } + } else { + // added coded for partial booking + $params = array( + 'date_from' => $dateFrom, + 'date_to' => $dateTo, + 'hotel_id' => $idHotel, + 'id_room_type' => $roomType['id_room_type'], + 'num_rooms' => $roomType['number_of_rooms'], + 'search_available' => 0, + 'search_partial' => 1, + 'search_booked' => 0, + 'search_unavai' => 0, + 'id_cart' => $this->context->cart->id, + 'id_guest' => null, + 'search_cart_rms' => 0, + 'occupancy' => $occupancy + ); + + $partiallyAvailableRooms = $objBookingDetail->getBookingData($params)['rm_data']; + $roomDayFragments = array(); // id_room => list of ['date_from' => .., 'date_to' => ..], one per day + + foreach ($partiallyAvailableRooms as $availableRooms) { + foreach ($availableRooms['data'] as $key => $availableRoom) { + if($key == 'partially_available') { + foreach ($availableRoom as $avlRoom) { + // all rooms free on this day, so several requested rooms fit side by side + foreach ($avlRoom['rooms'] as $avl_room) { + $roomDayFragments[$avl_room['id_room']][] = array( + 'date_from' => $avlRoom['date_from'], + 'date_to' => $avlRoom['date_to'], + ); + } + } + } + } + } + + // Merge contiguous day-fragments per room into continuous date ranges, so a room + // that has a gap in availability and later becomes available again still gets + // booked as one continuous range instead of fragmenting into multiple entries. + $partialRooms = array(); + foreach ($roomDayFragments as $idRoom => $fragments) { + usort($fragments, function ($a, $b) { + return strtotime($a['date_from']) <=> strtotime($b['date_from']); + }); + + $merged = null; + foreach ($fragments as $fragment) { + if ($merged !== null && $merged['date_to'] == $fragment['date_from']) { + $merged['date_to'] = $fragment['date_to']; + continue; + } + if ($merged !== null) { + $partialRooms[$idRoom.'_'.$merged['date_from'].'_'.$merged['date_to']] = array( + 'date_from' => $merged['date_from'], + 'date_to' => $merged['date_to'], + 'id_room' => $idRoom, + ); + } + $merged = $fragment; + } + if ($merged !== null) { + $partialRooms[$idRoom.'_'.$merged['date_from'].'_'.$merged['date_to']] = array( + 'date_from' => $merged['date_from'], + 'date_to' => $merged['date_to'], + 'id_room' => $idRoom, + ); + } + } + + // Cover each night once with the fewest room changes. Rooms this request already + // carted aren't seen as taken by the search, so pass them in. + $takenSegments = Db::getInstance()->executeS( + 'SELECT `id_room`, `date_from`, `date_to` FROM `'._DB_PREFIX_.'htl_cart_booking_data` + WHERE `id_cart` = '.(int) $this->context->cart->id.' AND `id_product` = '.(int) $roomType['id_room_type'] + ); + $partialRooms = $this->getPartialStayCoverage($partialRooms, $dateFrom, $dateTo, $takenSegments ?: array()); + if ($partialRooms === false) { + $this->error_msg = Tools::displayError('Requested room(s) not available'); + return false; + } + + if($partialRooms) { + // Calculate the requested total price and tax for proportional distribution + $requestedTotalPriceTaxExcl = 0; + if (isset($room['unit_price_without_tax'])) { + $requestedTotalPriceTaxExcl = $room['unit_price_without_tax']; + } else if (isset($room['total_price_with_tax']) && isset($room['total_tax'])) { + $requestedTotalPriceTaxExcl = $room['total_price_with_tax'] - $room['total_tax']; + } + $requestedTotalTax = isset($room['total_tax']) ? floatval($room['total_tax']) : 0; + $totalSplitNights = 0; + foreach ($partialRooms as $splitRoom) { + $totalSplitNights += HotelHelper::getNumberOfDays($splitRoom['date_from'], $splitRoom['date_to']); + } + if (!$totalSplitNights) { + $totalSplitNights = 1; + } + $remainingPriceTaxExcl = Tools::ps_round($requestedTotalPriceTaxExcl, _PS_PRICE_COMPUTE_PRECISION_); + $remainingTax = $requestedTotalTax; + $processedSplitRooms = 0; + $totalSplitRooms = count($partialRooms); + + foreach ($partialRooms as $avlRoom) { + if (!($idHtlCartBookingData = $objHotelCartBookingData->updateCartBooking( + $roomType['id_room_type'], + $occupancy, + 'up', + $idHotel, + $avlRoom['id_room'], + $avlRoom['date_from'], + $avlRoom['date_to'], + $roomServices, + $this->context->cart->id, + $this->context->cart->id_guest + ))) { + // A missing segment would leave nights of the stay unbooked. + $this->error_msg = Tools::displayError('Requested room(s) not available'); + return false; + } else { + $objCartBookingData = new HotelCartBookingData((int) $idHtlCartBookingData); + + /* Partial Booking indicator */ + Hook::exec( + 'actionAdminPartialBookingIndicator', + array( + 'objCartBookingData' => $objCartBookingData, + 'id_room_type' => $roomType['id_room_type'] + ) + ); + /* End */ + + // Calculate proportional price for this split segment + $processedSplitRooms++; + $segmentNights = HotelHelper::getNumberOfDays($avlRoom['date_from'], $avlRoom['date_to']); + if (!$segmentNights) { + $segmentNights = 1; + } + + // Distribute price proportionally by nights, give remainder to last segment + if ($processedSplitRooms == $totalSplitRooms) { + $segmentPriceTaxExcl = $remainingPriceTaxExcl; + $segmentTax = $remainingTax; + } else { + $segmentPriceTaxExcl = Tools::ps_round( + ($requestedTotalPriceTaxExcl / $totalSplitNights) * $segmentNights, + _PS_PRICE_COMPUTE_PRECISION_ + ); + $segmentTax = Tools::ps_round( + ($requestedTotalTax / $totalSplitNights) * $segmentNights, + _PS_PRICE_COMPUTE_PRECISION_ + ); + $remainingPriceTaxExcl -= $segmentPriceTaxExcl; + $remainingTax -= $segmentTax; + } + + // Create feature price for this split room segment + if ($segmentPriceTaxExcl) { + $room['unit_price_without_tax'] = $segmentPriceTaxExcl; + $this->wsFeaturePrices[] = $this->createFeaturePrice( + array( + 'id_product' => (int) $roomType['id_room_type'], + 'id_cart' => (int) $this->context->cart->id, + 'id_guest' => (int) $this->context->cart->id_guest, + 'date_from' => date('Y-m-d', strtotime($avlRoom['date_from'])), + 'date_to' => date('Y-m-d', strtotime($avlRoom['date_to'])), + 'id_room' => $objCartBookingData->id_room, + 'price' => $segmentPriceTaxExcl + ) + ); + } + + $dateRoomJoinKey = strtotime($avlRoom['date_from']).strtotime($avlRoom['date_to']).$objCartBookingData->id_product.$objCartBookingData->id_room; + // To update the price after valiate order is called. + $this->wsRequestedRoomTypes[$dateRoomJoinKey]['services'] = $roomServices; + $this->wsRequestedRoomTypes[$dateRoomJoinKey]['room'] = $room; + + // Store the split segment tax for later use + if (isset($room['id_tax_rules_group']) && Validate::isLoadedObject(new TaxRulesGroup((int) $room['id_tax_rules_group']))) { + $this->wsRequestedRooms[$dateRoomJoinKey]['id_tax_rules_group'] = $room['id_tax_rules_group']; + } elseif ($segmentTax) { + $this->wsRequestedRooms[$dateRoomJoinKey]['total_tax'] = $segmentTax; + } elseif (isset($room['total_tax'])) { + $this->wsRequestedRooms[$dateRoomJoinKey]['total_tax'] = $room['total_tax']; + } + } + } + } else { + return false; + } + + } + // end here + } + unset($room); + } else { + $roomWiseOccupancy = $occupancy; + if (isset($roomType['number_of_rooms'])) { + while ($roomType['number_of_rooms'] > 1) { + $roomWiseOccupancy[] = reset($occupancy); + $roomType['number_of_rooms']--; + } + } + + if ($quantityWise) { + $roomWiseOccupancy = count($roomWiseOccupancy); + } + + $roomServices = array(); + if (!$objHotelCartBookingData->updateCartBooking( + $roomType['id_room_type'], + $roomWiseOccupancy, + 'up', + $idHotel, + 0, + $dateFrom, + $dateTo, + $roomServices, + $this->context->cart->id, + $this->context->cart->id_guest + )) { + $this->error_msg = Tools::displayError('Requested room(s) not available'); + return false; + } + } + } + unset($roomType); + $this->removeAutoAddedServicesFromCart(); + return true; + } + + /** + * Picks free fragments that cover each night of the stay once, staying on the same room + * while it is free, else taking the longest free fragment. + * + * @param array $fragments free ranges, each ['id_room', 'date_from', 'date_to'] + * @param string $dateFrom stay check-in (Y-m-d) + * @param string $dateTo stay check-out (Y-m-d) + * @param array $takenSegments ranges already taken in the cart, each ['id_room', 'date_from', 'date_to'] + * + * @return array|false segments to book, each ['id_room', 'date_from', 'date_to'], or false + * when some night has no free room + */ + protected function getPartialStayCoverage($fragments, $dateFrom, $dateTo, $takenSegments = array()) + { + $stayEnd = strtotime(date('Y-m-d', strtotime($dateTo))); + $current = strtotime(date('Y-m-d', strtotime($dateFrom))); + $segments = array(); + $previousRoom = 0; + while ($current < $stayEnd) { + $best = false; + foreach ($fragments as $fragment) { + $fragmentFrom = strtotime(date('Y-m-d', strtotime($fragment['date_from']))); + $fragmentTo = min(strtotime(date('Y-m-d', strtotime($fragment['date_to']))), $stayEnd); + if ($fragmentFrom > $current || $fragmentTo <= $current) { + continue; + } + // Cut the fragment where the same room is already taken in the cart. + $isTaken = false; + foreach ($takenSegments as $taken) { + if ((int) $taken['id_room'] != (int) $fragment['id_room']) { + continue; + } + $takenFrom = strtotime(date('Y-m-d', strtotime($taken['date_from']))); + $takenTo = strtotime(date('Y-m-d', strtotime($taken['date_to']))); + if ($takenFrom <= $current && $takenTo > $current) { + $isTaken = true; + break; + } + if ($takenFrom > $current && $takenFrom < $fragmentTo) { + $fragmentTo = $takenFrom; + } + } + if ($isTaken) { + continue; + } + if ($best === false + || ((int) $fragment['id_room'] == $previousRoom && (int) $best['id_room'] != $previousRoom) + || ($fragmentTo > $best['to'] && !((int) $best['id_room'] == $previousRoom && (int) $fragment['id_room'] != $previousRoom)) + ) { + $best = array('id_room' => (int) $fragment['id_room'], 'to' => $fragmentTo); + } + } + if ($best === false) { + return false; + } + + $segments[] = array( + 'id_room' => $best['id_room'], + 'date_from' => date('Y-m-d', $current), + 'date_to' => date('Y-m-d', $best['to']), + ); + $previousRoom = $best['id_room']; + $current = $best['to']; + } + + return $segments; + } + + /** + * Core price calculation method for a per-booking or per-night service: PER_BOOKING/PER_DAY + * when this core defines them, else ONCE_FOR_BOOKING/CHECKIN_AND_DURING_STAY. + * + * @param bool $perNight + * + * @return int + */ + public static function getServicePriceCalculationMethod($perNight) + { + if ($perNight) { + return defined('Product::PRICE_CALCULATION_METHOD_PER_DAY') + ? Product::PRICE_CALCULATION_METHOD_PER_DAY + : Product::PRICE_CALCULATION_METHOD_CHECKIN_AND_DURING_STAY; + } + + return defined('Product::PRICE_CALCULATION_METHOD_PER_BOOKING') + ? Product::PRICE_CALCULATION_METHOD_PER_BOOKING + : Product::PRICE_CALCULATION_METHOD_ONCE_FOR_BOOKING; + } + + /** + * Whether the price calculation method bills the service for each night of the stay. + * + * @param int $priceCalculationMethod + * + * @return bool + */ + public static function isPerNightPriceCalculationMethod($priceCalculationMethod) + { + if (defined('Product::PRICE_CALCULATION_METHOD_PER_DAY')) { + return $priceCalculationMethod == Product::PRICE_CALCULATION_METHOD_PER_DAY; + } + + return !in_array((int) $priceCalculationMethod, array( + Product::PRICE_CALCULATION_METHOD_ON_CHECKIN_DAY, + Product::PRICE_CALCULATION_METHOD_ON_CHECKOUT_DAY, + Product::PRICE_CALCULATION_METHOD_CHECKIN_DAY_AND_CHECKOUT_DAY, + Product::PRICE_CALCULATION_METHOD_ONCE_FOR_BOOKING, + )); + } + + /** + * Days a service's unit price is billed for over a stay, as core computes it. + * + * @param int $priceCalculationMethod + * @param string $dateFrom + * @param string $dateTo + * + * @return int + */ + public static function getServiceBillableDays($priceCalculationMethod, $dateFrom, $dateTo) + { + if (method_exists('Product', 'getServicePriceBillableDays')) { + return (int) Product::getServicePriceBillableDays($priceCalculationMethod, $dateFrom, $dateTo); + } + + if (self::isPerNightPriceCalculationMethod($priceCalculationMethod)) { + return max((int) HotelHelper::getNumberOfDays($dateFrom, $dateTo), 1); + } + + return 1; + } + + /** + * Creating new service product for the request. + * @return int id of new created service product + */ + public function createWsService(array &$service, $idRoomType, $idServiceTaxRulesGroup = 0) + { + // A single service will be created and will be deleted after the booking is completed. + $objProduct = new Product(); + if(!trim($service['name']) || !Validate::isCatalogName(trim($service['name']))) { + $service['name'] = 'Api service'; + } + foreach (Language::getLanguages(false) as $language) { + $objProduct->name[$language['id_lang']] = $service['name']; + $linkRewrite = Tools::link_rewrite($service['name']); + $objProduct->link_rewrite[$language['id_lang']] = $linkRewrite; + } + $price = 0; + $quantity = 1; + if (isset($service['quantity'])) { + $quantity = $service['quantity']; + } + + if(isset($service['price_mode'])) { + if($service['price_mode'] == self::SERVICE_PRICE_MODE_PER_STAY) { + $service['price_mode'] = self::API_SERVICE_PRICE_MODE_PER_BOOKING; + } else if($service['price_mode'] == self::SERVICE_PRICE_MODE_PER_NIGHT) { + $service['price_mode'] = self::API_SERVICE_PRICE_MODE_PER_NIGHT; + } else if ($service['price_mode'] == self::SERVICE_PRICE_MODE_PER_PERSON_PER_NIGHT) { + // The caller already folded the per-person factor into quantity; bill per night. + $service['price_mode'] = self::API_SERVICE_PRICE_MODE_PER_NIGHT; + } else { + $service['price_mode'] = self::API_SERVICE_PRICE_MODE_PER_BOOKING; + } + } else { + $service['price_mode'] = self::API_SERVICE_PRICE_MODE_PER_BOOKING; + } + + $price = floatval($service['total_price_with_tax']) - floatval($service['total_tax']); + $service['total_price_without_tax'] = $price; + + if ($this->context->cart->id_currency != (int)Configuration::get('PS_CURRENCY_DEFAULT')) { + $currency = Currency::getCurrencyInstance($this->context->cart->id_currency); + $price = Tools::ps_round($price/$currency->conversion_rate, _PS_PRICE_COMPUTE_PRECISION_); + } + + + $objProduct->booking_product = false; + $objProduct->available_for_order = true; + $objProduct->id_category_default = Configuration::get('PS_SERVICE_CATEGORY'); + $objProduct->active = 1; + $objProduct->id_shop_default = Configuration::get('PS_SHOP_DEFAULT'); + $objProduct->indexed = false; + $objProduct->condition = 'new'; + $objProduct->price = $price; + $objProduct->out_of_stock = false; + $objProduct->id_tax_rules_group = $idServiceTaxRulesGroup; + $objProduct->is_virtual = 1; + $objProduct->show_price = true; + $objProduct->auto_add_to_cart = false; + $objProduct->show_at_front = false; + $objProduct->price_calculation_method = self::getServicePriceCalculationMethod( + $service['price_mode'] == self::API_SERVICE_PRICE_MODE_PER_NIGHT + ); + $objProduct->redirect_type = '404'; + $objProduct->visibility = 'none'; + $objProduct->minimal_quantity = 1; + $objProduct->selling_preference_type = Product::SELLING_PREFERENCE_WITH_ROOM_TYPE; + $objProduct->allow_multiple_quantity = true; + $objProduct->max_quantity = $quantity; + if ($objProduct->save()) { + $objProduct->updateCategories(array( + Configuration::get('PS_SERVICE_CATEGORY') + )); + $objRoomTypeServiceProduct = new RoomTypeServiceProduct(); + $objRoomTypeServiceProduct->addRoomProductLink( + $objProduct->id, + array($idRoomType), + RoomTypeServiceProduct::WK_ELEMENT_TYPE_ROOM_TYPE + ); + + StockAvailable::setQuantity($objProduct->id, 0, 99999999); + $this->wsIdServices[] = $objProduct->id; + } + + return $objProduct->id; + } + + /** + * Removing the non requested auto added services. + */ + public function removeAutoAddedServicesFromCart() + { + if (Validate::isLoadedobject($this->context->cart)) { + $this->context->cart->getProducts(true); + $objServiceProductCartDetail = new ServiceProductCartDetail(); + if ($serviceProducts = $objServiceProductCartDetail->getServiceProductsInCart( + $this->context->cart->id, + [], + 0, + null, + null, + null, + null, + null, + 0, + 1 + )) { + foreach ($serviceProducts as $serviceProduct) { + // Keyed by room type id ('id_room_type'), like wsRequestedRoomTypes. + $dateRoomJoinKey = strtotime($serviceProduct['date_from']).strtotime($serviceProduct['date_to']).$serviceProduct['id_room_type'].$serviceProduct['id_room']; + // Checking if the auto add service was sent in the request + if (!isset($this->wsRequestedRoomTypes[$dateRoomJoinKey]['services'][$serviceProduct['id_product']]) + && ($idServiceProductCartDetail = $objServiceProductCartDetail->alreadyExists( + $this->context->cart->id, + $serviceProduct['id_product'], + $serviceProduct['id_hotel_cart_booking']) + )) { + $objServiceProductCartDetail = new ServiceProductCartDetail((int) $idServiceProductCartDetail); + // Remove quantity x billable days, else a 0-priced line stays in the cart. + $this->context->cart->updateQty( + $objServiceProductCartDetail->quantity * self::getServiceBillableDays( + $serviceProduct['price_calculation_method'], + $serviceProduct['date_from'], + $serviceProduct['date_to'] + ), + $objServiceProductCartDetail->id_product, + null, + false, + 'down' + ); + $objServiceProductCartDetail->delete(); + } + } + } + } + } + + /** + * Adding the cart rules, after creating them. + */ + public function applyCartRules($params) + { + $error = ''; + if ($cartRules = $this->formatCartRulesInRequestData($params)) { + if ($cartRules = $this->createCartRules($cartRules)) { + foreach ($cartRules as $cartRule) { + $objCartRule = new CartRule((int) $cartRule); + if (($error = $objCartRule->checkValidity($this->context)) + && $error != '' + ) { + break; + } else { + $this->context->cart->addCartRule($objCartRule->id); + } + } + } + } + + return $error; + } + + /** + * Getting cart rules and creating new ones if they do not exists. + */ + public function createCartRules($requestedCartRules) + { + $cartRules = array(); + if ($requestedCartRules) { + $languags = Language::getIDs(true); + foreach ($requestedCartRules as $cartRule) { + if (Validate::isLoadedObject($objCartRule = new CartRule(CartRule::getIdByCode($cartRule['code'])))) { + $cartRules[] = $objCartRule->id; + } else { + $idCurrency = $this->context->currency->id; + if (isset($cartRule['currency']) + && ($selectedCurrency = Currency::getIdByIsoCode($cartRule['currency'])) + && Validate::isLoadedObject($objCurrency = new Currency($selectedCurrency)) + && $objCurrency->active + ) { + $idCurrency = $objCurrency->id; + } + + $objCartRule = new CartRule(); + foreach ($languags as $idLang) { + $objCartRule->name[$idLang] = $cartRule['code']; + } + + $objCartRule->quantity = 1; + $objCartRule->quantity_per_user = 1; + $objCartRule->id_customer = $this->bookingCustomer->id; + $objCartRule->highlight = 1; + $objCartRule->date_from = date('Y-m-d H:i:s'); + $objCartRule->date_to = date('Y-m-d H:i:s', strtotime($objCartRule->date_from) + (3600 * 24 * 365.25));; + $objCartRule->active = 1; + $objCartRule->reduction_tax = false; + $objCartRule->minimum_amount_currency = $idCurrency; + $objCartRule->reduction_currency = $idCurrency; + $objCartRule->code = $cartRule['code']; + if ($cartRule['type'] == self::API_CART_RULE_VALUE_TYPE_PERCENTAGE) { + $objCartRule->reduction_percent = $cartRule['value']; + } else if ($cartRule['type'] == self::API_CART_RULE_VALUE_TYPE_AMOUNT) { + $objCartRule->reduction_amount = $cartRule['value']; + } + + if ($objCartRule->add()) { + $cartRules[] = $objCartRule->id; + $this->wsCartRules[] = $objCartRule->id; + } + } + } + } + + return $cartRules; + } + + + public function addRoomsInOrder($idOrder, $roomTypes) + { + $objOrder = new Order($idOrder); + $objHotelBookingDetail = new HotelBookingDetail(); + $objRoomType = new HotelRoomType(); + $this->createCartForOrder($objOrder->id); + if (!$this->addRoomsInCart($roomTypes, $objOrder->id_address_tax)) { + return false; + } + $idOrderInvoice = false; + if (($objOrderInvoice = $objOrder->getInvoicesCollection()->getFirst()) + && Validate::isLoadedObject($objOrderInvoice) + ) { + $idOrderInvoice = $objOrderInvoice->id; + } + + $objCart = $this->context->cart; + $objOrderDetail = new OrderDetail(); + $objOrderDetail->createList($objOrder, $objCart, $objOrder->getCurrentOrderState(), $objCart->getProducts(true), $idOrderInvoice); + + // update totals amount of order + // creating the new object to reload the data changes made while removing the rooms. + $objOrder = new Order($idOrder); + $objOrder->total_products += (float)$objCart->getOrderTotal(false, Cart::ONLY_ROOMS); + $objOrder->total_products_wt += (float)$objCart->getOrderTotal(true, Cart::ONLY_ROOMS); + $objOrder->total_paid += Tools::ps_round((float)($objCart->getOrderTotal(true, Cart::ONLY_ROOMS)), _PS_PRICE_COMPUTE_PRECISION_); + $objOrder->total_paid_tax_excl += Tools::ps_round((float)($objCart->getOrderTotal(false, Cart::ONLY_ROOMS)), _PS_PRICE_COMPUTE_PRECISION_); + $objOrder->total_paid_tax_incl += Tools::ps_round((float)($objCart->getOrderTotal(true, Cart::ONLY_ROOMS)), _PS_PRICE_COMPUTE_PRECISION_); + $objOrder->total_discounts += (float)abs($objCart->getOrderTotal(true, Cart::ONLY_DISCOUNTS)); + $objOrder->total_discounts_tax_excl += (float)abs($objCart->getOrderTotal(false, Cart::ONLY_DISCOUNTS)); + $objOrder->total_discounts_tax_incl += (float)abs($objCart->getOrderTotal(true, Cart::ONLY_DISCOUNTS)); + + // Save changes of order + $res = $objOrder->update(); + $objAddress = new Address((int) $objOrder->id_address_tax); + $idLang = (int) $this->context->cart->id_lang; + $processedRoomTypeIds = array(); + foreach ($roomTypes as $roomType) { + if (isset($processedRoomTypeIds[$roomType['id_room_type']])) { + continue; + } + $processedRoomTypeIds[$roomType['id_room_type']] = true; + $orderDetails = $objHotelBookingDetail->getPsOrderDetailsByProduct($roomType['id_room_type'], $objOrder->id); + $idOrderDetail = end($orderDetails)['id_order_detail']; // to get the max id_order_detail + $objCartBookingData = new HotelCartBookingData(); + if ($cartBookingData = $objCartBookingData->getOnlyCartBookingData( + $this->context->cart->id, + $this->context->cart->id_guest, + $roomType['id_room_type'] + )) { + + foreach ($cartBookingData as $cb_k => $cb_v) { + + $objCartBookingData = new HotelCartBookingData($cb_v['id']); + $objCartBookingData->id_order = $objOrder->id; + // $objCartBookingData->save(); + $objBookingDetail = new HotelBookingDetail(); + $objBookingDetail->id_product = $roomType['id_room_type']; + $objBookingDetail->id_order = $objOrder->id; + $objBookingDetail->id_order_detail = $idOrderDetail; + $objBookingDetail->id_cart = $this->context->cart->id; + $objBookingDetail->id_room = $objCartBookingData->id_room; + $objBookingDetail->id_hotel = $objCartBookingData->id_hotel; + $objBookingDetail->id_customer = $objOrder->id_customer; + $objBookingDetail->booking_type = $objCartBookingData->booking_type; + $objBookingDetail->id_status = 1; + $objBookingDetail->comment = $objCartBookingData->comment; + $objBookingDetail->room_type_name = Product::getProductName($roomType['id_room_type'], null, $objOrder->id_lang); + + $objBookingDetail->date_from = $objCartBookingData->date_from; + $objBookingDetail->date_to = $objCartBookingData->date_to; + $objBookingDetail->adults = $objCartBookingData->adults; + $objBookingDetail->children = $objCartBookingData->children; + $objBookingDetail->child_ages = $objCartBookingData->child_ages; + + $total_price = HotelRoomTypeFeaturePricing::getRoomTypeTotalPrice( + $roomType['id_room_type'], + $objCartBookingData->date_from, + $objCartBookingData->date_to, + 0, + Group::getCurrent()->id, + $this->context->cart->id, + $this->context->cart->id_guest, + $objCartBookingData->id_room, + 0 + ); + + $objBookingDetail->total_price_tax_excl = $total_price['total_price_tax_excl']; + $objBookingDetail->total_price_tax_incl = $total_price['total_price_tax_incl']; + $objBookingDetail->total_paid_amount = Tools::ps_round($total_price['total_price_tax_incl'], _PS_PRICE_COMPUTE_PRECISION_); + if ($idOrderInvoice) { + $objOrderInvoice = new OrderInvoice($idOrderInvoice); + $objOrderInvoice->total_paid_tax_excl += Tools::ps_round($total_price['total_price_tax_excl'], _PS_PRICE_COMPUTE_PRECISION_); + $objOrderInvoice->total_paid_tax_incl += Tools::ps_round($total_price['total_price_tax_incl'], _PS_PRICE_COMPUTE_PRECISION_); + $objOrderInvoice->total_products += Tools::ps_round($total_price['total_price_tax_excl'], _PS_PRICE_COMPUTE_PRECISION_); + $objOrderInvoice->total_products_wt += Tools::ps_round($total_price['total_price_tax_incl'], _PS_PRICE_COMPUTE_PRECISION_); + $objOrderInvoice->update(); + } + + // Save hotel information/location/contact + if (Validate::isLoadedObject($objRoom = new HotelRoomInformation($objCartBookingData->id_room))) { + $objBookingDetail->room_num = $objRoom->room_num; + } + + if (Validate::isLoadedObject($objHotelBranch = new HotelBranchInformation( + $objCartBookingData->id_hotel, + $idLang + ))) { + $addressInfo = $objHotelBranch->getAddress($objCartBookingData->id_hotel); + $objBookingDetail->hotel_name = $objHotelBranch->hotel_name; + $objBookingDetail->city = $addressInfo['city']; + $objBookingDetail->state = State::getNameById($addressInfo['id_state']); + $objBookingDetail->country = Country::getNameById($idLang, $addressInfo['id_country']); + $objBookingDetail->zipcode = $addressInfo['postcode'];; + $objBookingDetail->phone = $addressInfo['phone']; + $objBookingDetail->email = $objHotelBranch->email; + $objBookingDetail->check_in_time = $objHotelBranch->check_in; + $objBookingDetail->check_out_time = $objHotelBranch->check_out; + } + + if ($roomTypeInfo = $objRoomType->getRoomTypeInfoByIdProduct($roomType['id_room_type'])) { + $objBookingDetail->adults = $objCartBookingData->adults; + $objBookingDetail->children = $objCartBookingData->children; + $objBookingDetail->child_ages = $objCartBookingData->child_ages; + } + + $rateDateRoomJoinKey = strtotime($objCartBookingData->date_from).strtotime($objCartBookingData->date_to).$objCartBookingData->id_product.$objCartBookingData->id_room; + $objBookingDetail->rate_plan_name = isset($this->wsRequestedRoomTypes[$rateDateRoomJoinKey]['room']['rate_plan_name']) + ? $this->wsRequestedRoomTypes[$rateDateRoomJoinKey]['room']['rate_plan_name'] + : 'Not Available'; + + if ($objBookingDetail->save()) { + $objServiceProductCartDetail = new ServiceProductCartDetail(); + $dateRoomJoinKey = strtotime($objCartBookingData->date_from).strtotime($objCartBookingData->date_to).$objCartBookingData->id_product.$objCartBookingData->id_room; + + if (isset($this->wsRequestedRoomTypes[$dateRoomJoinKey]['services']) + && ($services = $objServiceProductCartDetail->getServiceProductsInCart($objCartBookingData->id_cart, + [], + null, + $objCartBookingData->id)) + ) { + foreach ($services as $service) { + $insertedServiceProductIdOrderDetail = $objBookingDetail->getLastInsertedServiceIdOrderDetail($objOrder->id, $service['id_product']); + $numDays = self::getServiceBillableDays( + Product::getProductPriceCalculation($service['id_product']), + $objBookingDetail->date_from, + $objBookingDetail->date_to + ); + $totalPriceTaxExcl = Product::getServiceProductPrice( + $service['id_product'], + 0, + false, + $roomTypeInfo['id'], + false, + $service['quantity'], + $objBookingDetail->date_from, + $objBookingDetail->date_to + ); + $totalPriceTaxIncl = Product::getServiceProductPrice( + $service['id_product'], + 0, + false, + $roomTypeInfo['id'], + true, + $service['quantity'], + $objBookingDetail->date_from, + $objBookingDetail->date_to + ); + $serviceQuantityDivisor = $numDays * $service['quantity']; + $unitPriceTaxExcl = $serviceQuantityDivisor > 0 ? $totalPriceTaxExcl / $serviceQuantityDivisor : 0; + $unitPriceTaxIncl = $serviceQuantityDivisor > 0 ? $totalPriceTaxIncl / $serviceQuantityDivisor : 0; + if ($unitPriceTaxExcl > 0) { + $taxMultiplier = $unitPriceTaxIncl / $unitPriceTaxExcl; + } else { + $taxMultiplier = 1; + } + + $quantity = $service['quantity'] * $numDays; + $objOrderDetail = new OrderDetail($insertedServiceProductIdOrderDetail); + $totalPriceTaxExclOld = $totalPriceTaxExcl; + $totalPriceTaxInclOld = $totalPriceTaxIncl; + if (isset($this->wsRequestedRoomTypes[$dateRoomJoinKey]['services'][$service['id_product']]) + && isset($this->wsRequestedRoomTypes[$dateRoomJoinKey]['services'][$service['id_product']]['unit_price_without_tax']) + ) { + $totalPriceTaxExcl = 0; + $totalPriceTaxIncl = 0; + if ((int) $this->wsRequestedRoomTypes[$dateRoomJoinKey]['services'][$service['id_product']]['unit_price_without_tax']) { + $totalPriceTaxExcl = $this->wsRequestedRoomTypes[$dateRoomJoinKey]['services'][$service['id_product']]['unit_price_without_tax']; + $totalPriceTaxIncl = $this->wsRequestedRoomTypes[$dateRoomJoinKey]['services'][$service['id_product']]['unit_price_without_tax'] * $taxMultiplier; + } + + if ($totalPriceTaxExcl > 0 && $quantity > 0) { + $unitPriceTaxExcl = $totalPriceTaxExcl / $quantity; + $unitPriceTaxIncl = $totalPriceTaxIncl / $quantity; + } + } else if (isset($this->wsRequestedRoomTypes[$dateRoomJoinKey]['services'][$service['id_product']]['total_price_without_tax'])) { + $totalPriceTaxExcl = $this->wsRequestedRoomTypes[$dateRoomJoinKey]['services'][$service['id_product']]['total_price_without_tax']; + $totalPriceTaxIncl = $totalPriceTaxExcl * $taxMultiplier; + + $unitPriceTaxExcl = $quantity > 0 ? $totalPriceTaxExcl / $quantity : 0; + $unitPriceTaxIncl = $quantity > 0 ? $totalPriceTaxIncl / $quantity : 0; + } + + $objAddress = new Address((int) $objOrder->id_address_tax); + if (!empty($this->wsRequestedRoomTypes[$dateRoomJoinKey]['services'][$service['id_product']]['total_tax'])) { + if (!isset($this->wsRequestedRoomTypes[$dateRoomJoinKey]['services'][$service['id_product']]['id_tax_rules_group'])) { + $this->wsRequestedRoomTypes[$dateRoomJoinKey]['services'][$service['id_product']]['id_tax_rules_group'] = $this->createTaxRulesGroup($totalPriceTaxExcl > 0 ? ($this->wsRequestedRoomTypes[$dateRoomJoinKey]['services'][$service['id_product']]['total_tax']/$totalPriceTaxExcl)*100 : 0, false, $objAddress); + } + // Use the sent total_tax, not a recomputed rate; the group is kept for tax rows. + $totalPriceTaxIncl = Tools::ps_round($totalPriceTaxExcl + $this->wsRequestedRoomTypes[$dateRoomJoinKey]['services'][$service['id_product']]['total_tax'], _PS_PRICE_COMPUTE_PRECISION_); + $unitPriceTaxIncl = Tools::ps_round($quantity > 0 ? ($totalPriceTaxIncl / $quantity) : 0, _PS_PRICE_COMPUTE_PRECISION_); + } elseif (isset($this->wsRequestedRoomTypes[$dateRoomJoinKey]['services'][$service['id_product']]['id_tax_rules_group']) && $this->wsRequestedRoomTypes[$dateRoomJoinKey]['services'][$service['id_product']]['id_tax_rules_group']) { + $objTaxManager = TaxManagerFactory::getManager($objAddress, $this->wsRequestedRoomTypes[$dateRoomJoinKey]['services'][$service['id_product']]['id_tax_rules_group']); + $objTaxCalculator = $objTaxManager->getTaxCalculator(); + $unitPriceTaxIncl = $objTaxCalculator->addTaxes($unitPriceTaxExcl); + + $unitPriceTaxIncl = Tools::ps_round($unitPriceTaxIncl, _PS_PRICE_COMPUTE_PRECISION_); + $totalPriceTaxIncl = Tools::ps_round(($unitPriceTaxIncl * $quantity), _PS_PRICE_COMPUTE_PRECISION_); + } + + $priceDiffTaxExcl = Tools::ps_round($totalPriceTaxExcl - $totalPriceTaxExclOld, _PS_PRICE_COMPUTE_PRECISION_); + $priceDiffTaxIncl = Tools::ps_round($totalPriceTaxIncl - $totalPriceTaxInclOld, _PS_PRICE_COMPUTE_PRECISION_) ; + $objOrderDetail->total_price_tax_excl += $priceDiffTaxExcl; + $objOrderDetail->total_price_tax_incl += $priceDiffTaxIncl; + + $objOrderDetail->unit_price_tax_excl = Tools::ps_round(($objOrderDetail->total_price_tax_excl / $objOrderDetail->product_quantity), _PS_PRICE_COMPUTE_PRECISION_); + $objOrderDetail->unit_price_tax_incl = Tools::ps_round(($objOrderDetail->total_price_tax_incl / $objOrderDetail->product_quantity), _PS_PRICE_COMPUTE_PRECISION_); + $objOrderDetail->save(); + + $objServiceProductOrderDetail = new ServiceProductOrderDetail(); + $objServiceProductOrderDetail->id_product = $service['id_product']; + $objServiceProductOrderDetail->id_order = $objBookingDetail->id_order; + $objServiceProductOrderDetail->id_order_detail = $insertedServiceProductIdOrderDetail; + $objServiceProductOrderDetail->id_cart = $this->context->cart->id; + $objServiceProductOrderDetail->id_htl_booking_detail = $objBookingDetail->id; + $objServiceProductOrderDetail->unit_price_tax_excl = Tools::ps_round($unitPriceTaxExcl, _PS_PRICE_COMPUTE_PRECISION_); + $objServiceProductOrderDetail->unit_price_tax_incl = Tools::ps_round($unitPriceTaxIncl, _PS_PRICE_COMPUTE_PRECISION_); + $objServiceProductOrderDetail->total_price_tax_excl = Tools::ps_round($totalPriceTaxExcl, _PS_PRICE_COMPUTE_PRECISION_); + $objServiceProductOrderDetail->total_price_tax_incl = Tools::ps_round($totalPriceTaxIncl, _PS_PRICE_COMPUTE_PRECISION_); + $objServiceProductOrderDetail->name = $service['name']; + $objServiceProductOrderDetail->quantity = $service['quantity']; + $objServiceProductOrderDetail->save(); + + $objOrder->total_products += $objServiceProductOrderDetail->total_price_tax_excl; + $objOrder->total_products_wt += $objServiceProductOrderDetail->total_price_tax_incl; + + if ($idOrderInvoice) { + $objOrderInvoice = new OrderInvoice($idOrderInvoice); + $objOrderInvoice->total_paid_tax_excl += Tools::ps_round($totalPriceTaxExcl, _PS_PRICE_COMPUTE_PRECISION_); + $objOrderInvoice->total_paid_tax_incl += Tools::ps_round($totalPriceTaxIncl, _PS_PRICE_COMPUTE_PRECISION_); + $objOrderInvoice->total_products += Tools::ps_round($totalPriceTaxExcl, _PS_PRICE_COMPUTE_PRECISION_); + $objOrderInvoice->total_products_wt += Tools::ps_round($totalPriceTaxIncl, _PS_PRICE_COMPUTE_PRECISION_); + $objOrderInvoice->update(); + } + + $objOrder->total_paid += Tools::ps_round($totalPriceTaxIncl, _PS_PRICE_COMPUTE_PRECISION_); + $objOrder->total_paid_tax_excl += Tools::ps_round($totalPriceTaxExcl, _PS_PRICE_COMPUTE_PRECISION_); + $objOrder->total_paid_tax_incl += Tools::ps_round($totalPriceTaxIncl, _PS_PRICE_COMPUTE_PRECISION_); + + $isAutoAdded = false; + if ($objOrderDetail->product_auto_add && $objOrderDetail->product_price_addition_type == Product::PRICE_ADDITION_TYPE_WITH_ROOM) { + $isAutoAdded = true; + } + + if (!$isAutoAdded + && isset($this->wsRequestedRoomTypes[$dateRoomJoinKey]['services'][$service['id_product']]['id_tax_rules_group']) + && $this->wsRequestedRoomTypes[$dateRoomJoinKey]['services'][$service['id_product']]['id_tax_rules_group'] + ) { + $this->saveTaxCalculator($objOrderDetail->id, $this->wsRequestedRoomTypes[$dateRoomJoinKey]['services'][$service['id_product']]['id_tax_rules_group'], 0, $objServiceProductOrderDetail->id); + } + } + + $objOrder->save(); + } + } + } + } + } + + $this->deleteWsFeaturePrices(); + if(isset($this->context->cart) && $this->context->cart->id) { + HotelRoomTypeFeaturePricing::deleteFeaturePrices($this->context->cart->id); + } + + return true; + } + + + public function formatCartRulesInRequestData($data) + { + $formattedCartRules = array(); + if (isset($data['cart_rules']['cart_rule'][0])) { + $formattedCartRules = $data['cart_rules']['cart_rule']; + } elseif (isset($data['cart_rules']) + && !isset($data['cart_rules'][0]) + && isset($data['cart_rules']['cart_rule']) + ) { + $formattedCartRules[] = $data['cart_rules']['cart_rule']; + } elseif (isset($data['cart_rules'][0])) { + $formattedCartRules = $data['cart_rules']; + } + + return $formattedCartRules; + } + + public function getPostRequest($head = false) + { + $putresource = fopen('php://input', 'r'); + $inputXML = ''; + while ($putData = fread($putresource, 1024)) { + $inputXML .= $putData; + } + + fclose($putresource); + + return json_decode($inputXML, true); + } + + + // Adjust the price to 0 when the booking is cancelled or refunded + public function adjustPriceForOrderReversal($idOrder, $params) + { + $objOrder = new Order((int)$idOrder); + $objServiceProductOrderDetail = new ServiceProductOrderDetail(); + $objHotelBookingDetail = new HotelBookingDetail(); + if(isset($params['price_details']['total_price_with_tax'])){ + if ($roomsInOrder = $objHotelBookingDetail->getOrderCurrentDataByOrderId($objOrder->id)) { + foreach ($roomsInOrder as $roomInfo) { + $objHotelBookingDetail = new HotelBookingDetail((int) $roomInfo['id']); + $objHotelBookingDetail->total_price_tax_incl = 0; + $objHotelBookingDetail->total_price_tax_excl = 0; + $objHotelBookingDetail->unit_price_tax_incl = 0; + $objHotelBookingDetail->unit_price_tax_excl = 0; + $objHotelBookingDetail->total_paid_amount = 0; + $objHotelBookingDetail->save(); + + // Updating the price + $objOrderDetail = new OrderDetail((int) $objHotelBookingDetail->id_order_detail); + $orderDetailsList[] = $objOrderDetail->id; + $objOrderDetail->total_price_tax_incl = 0; + $objOrderDetail->total_price_tax_excl = 0; + $objOrderDetail->unit_price_tax_incl = 0; + $objOrderDetail->unit_price_tax_excl = 0; + $objOrderDetail->save(); + + // Update OrderInvoice of this OrderDetail + if ($objOrderDetail->id_order_invoice != 0) { + // values changes as values are calculated accoding to the quantity of the product by webkul + $objOrderInvoice = new OrderInvoice($objOrderDetail->id_order_invoice); + $objOrderInvoice->total_paid_tax_excl = 0; + $objOrderInvoice->total_paid_tax_incl = 0; + $objOrderInvoice->total_products = 0; + $objOrderInvoice->total_products_wt = 0; + $objOrderInvoice->update(); + } + } + + $objOrder->total_paid_tax_incl = 0; + $objOrder->total_products_wt = 0; + $objOrder->total_products = 0; + $objOrder->total_paid_tax_excl = 0; + $objOrder->total_paid_real = 0; + $objOrder->total_paid = 0; + $objOrder->save(); + + if($services = $objServiceProductOrderDetail->getroomTypeServiceProducts((int)$objOrder->id)){ + foreach ($services as $key => $service) { + if(isset($service['additional_services']) && $service['additional_services']){ + foreach($service['additional_services'] as $additionalService){ + if($idServiceProductOrderDetail = $additionalService['id_service_product_order_detail']){ + if(Validate::isLoadedObject($objServiceProductOrderDetail = new ServiceProductOrderDetail((int) $idServiceProductOrderDetail))) { + // Updating the price + $objOrderDetail = new OrderDetail((int) $objServiceProductOrderDetail->id_order_detail); + $orderDetailsList[] = $objOrderDetail->id; + $objOrderDetail->total_price_tax_incl = 0; + $objOrderDetail->total_price_tax_excl = 0; + $objOrderDetail->unit_price_tax_incl = 0; + $objOrderDetail->unit_price_tax_excl = 0; + $objOrderDetail->save(); + + $objServiceProductOrderDetail->unit_price_tax_excl = 0; + $objServiceProductOrderDetail->unit_price_tax_incl = 0; + $objServiceProductOrderDetail->total_price_tax_excl = 0; + $objServiceProductOrderDetail->total_price_tax_incl = 0; + $objServiceProductOrderDetail->save(); + } + } + } + } + } + } + + if($orderDetailsList) { + // Module tax rows live in order_tax_detail (order_detail_tax is gone). + Db::getInstance()->execute('DELETE FROM `' . _DB_PREFIX_ . 'order_tax_detail` WHERE id_order_detail IN ('.implode(',', array_map('intval', $orderDetailsList)).')'); + } + + } + } + + } + + /** + * Deleting the service from the order. + */ + public function removeServicesFromOrderedRoom($services) + { + if (count($services)) { + foreach ($services as $service) { + $idServiceProductOrderDetail = $service['id_service_product_order_detail']; + if (Validate::isLoadedObject($objServiceProductOrderDetail = new ServiceProductOrderDetail((int) $idServiceProductOrderDetail))) { + $objOrderDetail = new OrderDetail((int) $objServiceProductOrderDetail->id_order_detail); + $priceTaxExcl = $objServiceProductOrderDetail->total_price_tax_excl; + $priceTaxIncl = $objServiceProductOrderDetail->total_price_tax_incl; + $objHotelBookingDetail = new HotelBookingDetail($objServiceProductOrderDetail->id_htl_booking_detail); + $numDays = self::getServiceBillableDays( + Product::getProductPriceCalculation($service['id_product']), + $objHotelBookingDetail->date_from, + $objHotelBookingDetail->date_to + ); + + $quantity = $objOrderDetail->product_quantity * $numDays; + if ($objServiceProductOrderDetail->delete()) { + $objOrder = new Order($objServiceProductOrderDetail->id_order); + // Delete the shared order_detail only when no service still uses it. + $remainingServices = (int) Db::getInstance()->getValue( + 'SELECT COUNT(*) FROM `'._DB_PREFIX_.'service_product_order_detail` WHERE id_order_detail = '.(int) $objOrderDetail->id + ); + if ($quantity >= $objOrderDetail->product_quantity && $remainingServices === 0) { + $objOrderDetail->delete(); + } else { + $objOrderDetail->product_quantity = max($objOrderDetail->product_quantity - $quantity, 0); + + $objOrderDetail->total_price_tax_excl -= $priceTaxExcl; + $objOrderDetail->total_price_tax_incl -= $priceTaxIncl; + + $objOrderDetail->updateTaxAmount($objOrder); + + $objOrderDetail->update(); + } + + $objOrder->total_paid_tax_excl -= $priceTaxExcl; + $objOrder->total_paid_tax_incl -= $priceTaxIncl; + $objOrder->total_paid -= $priceTaxIncl; + + $objOrder->update(); + } + } + } + } + } + + + protected function applyDiscountOnInvoice($objOrderInvoice, $valueTaxIncl, $valueTaxExcl) + { + // Update OrderInvoice + $objOrderInvoice->total_discount_tax_incl += $valueTaxIncl; + $objOrderInvoice->total_discount_tax_excl += $valueTaxExcl; + $objOrderInvoice->total_paid_tax_incl -= $valueTaxIncl; + $objOrderInvoice->total_paid_tax_excl -= $valueTaxExcl; + $objOrderInvoice->update(); + } + + /** + * Adding the cart customer. + */ + public function processCustomer($customerDetails) + { + $this->context->cookie->id_guest = $this->context->cart->id_guest; + if (!isset($this->bookingCustomer->id)) { + $objCustomer = new Customer(); + $objCustomer->firstname = $customerDetails['firstname']; + $objCustomer->lastname = $customerDetails['lastname']; + $objCustomer->email = $customerDetails['email']; + $objCustomer->passwd = md5(time()._COOKIE_KEY_); + $objCustomer->phone = (isset($customerDetails['phone']) ? $customerDetails['phone'] : ''); + $objCustomer->cleanGroups(); + $objCustomer->add(); + $this->bookingCustomer = $objCustomer; + } else { + if (isset($customerDetails['firstname']) && Validate::isName($customerDetails['firstname'])) { + $this->bookingCustomer->firstname = $customerDetails['firstname']; + } + + if (isset($customerDetails['lastname']) && Validate::isName($customerDetails['lastname'])) { + $this->bookingCustomer->lastname = $customerDetails['lastname']; + } + + if (isset($customerDetails['email']) && Validate::isEmail($customerDetails['email'])) { + $this->bookingCustomer->email = $customerDetails['email']; + } + + if (isset($customerDetails['phone']) && Validate::isPhoneNumber($customerDetails['phone'])) { + $this->bookingCustomer->phone = $customerDetails['phone']; + } + + $this->bookingCustomer->save(); + } + + // Since the address is validated if even a single address field is present in the request. + if (isset($customerDetails['country_code']) + && $customerDetails['country_code'] + ) { + $customerDetails['id_country'] = Country::getByIso($customerDetails['country_code']); + $objCountry = new Country($customerDetails['id_country']); + if ($objCountry->contains_states) { + $customerDetails['id_state'] = State::getIdByIso($customerDetails['state_code']); + } + + $active = true; + $cache_id = 'Address::getFirstCustomerAddressId_'.(int) $this->bookingCustomer->id.'-'.(bool)$active; + Cache::clean($cache_id); + if ($idAddress = Address::getFirstCustomerAddressId($this->bookingCustomer->id)) { + $objAddress = new Address((int) $idAddress); + } else { + $objAddress = new Address(); + $objAddress->alias = 'Generated by bookings API'; + $objAddress->id_customer = $this->bookingCustomer->id; + $objAddress->auto_generated = true; + } + + $objAddress->firstname = $customerDetails['firstname']; + $objAddress->lastname = $customerDetails['lastname']; + if (isset($customerDetails['phone'])) { + $objAddress->phone = $customerDetails['phone']; + } + + $objAddress->address1 = $customerDetails['address']; + $objAddress->city = $customerDetails['city']; + $objAddress->postcode = isset($customerDetails['zip']) ? $customerDetails['zip'] : ''; + $objAddress->id_country = $customerDetails['id_country']; + $objAddress->id_state = isset($customerDetails['id_state']) ? $customerDetails['id_state'] : 0; + + $objAddress->save(); + + // Clear the cached "no address" so the cart uses the new guest address. + Cache::clean($cache_id); + if (Validate::isLoadedObject($objAddress) && Validate::isLoadedObject($this->context->cart)) { + $this->context->cart->id_address_invoice = (int) $objAddress->id; + $this->context->cart->id_address_delivery = (int) $objAddress->id; + $this->context->cart->save(); + } + } + + // to remove the older non ordered cart for this customer. + $objRequestCart = $this->context->cart; + $idFreshCart = $objRequestCart->id; + $this->context->cookie->id_cart = $idFreshCart; + $this->context->updateCustomer($this->bookingCustomer, 1); + + // updateCustomer() may swap in an abandoned cart (PS_CART_FOLLOWING): restore ours and bind + // it to the customer, else validateOrder() fails on secure key. Unsaved (modify) carts are only restored. + if (!$idFreshCart) { + $this->context->cart = $objRequestCart; + } elseif ($this->context->cart->id != $idFreshCart) { + $objFreshCart = new Cart($idFreshCart); + $objFreshCart->secure_key = $this->bookingCustomer->secure_key; + $objFreshCart->id_customer = (int) $this->bookingCustomer->id; + $objFreshCart->id_guest = (int) $this->context->cookie->id_guest; + $objFreshCart->id_address_invoice = (int) Address::getFirstCustomerAddressId((int) $this->bookingCustomer->id); + $objFreshCart->save(); + $objFreshCart->autosetProductAddress(); + $this->context->cart = $objFreshCart; + $this->context->cookie->id_cart = $idFreshCart; + } + } + + + /** + * Validating the PUT request. + */ + public function validatePutRequest($params) + { + $objCustomer = new Customer(); + $this->error_msg = ''; + if (!isset($params['id_booking']) || !$params['id_booking']) { + $this->error_msg = Tools::displayError('id is required with PUT requests.'); + } elseif (!Validate::isLoadedObject(new Order($params['id_booking']))) { + $this->error_msg = Tools::displayError('Booking not found.'); + } elseif (!isset($params['booking_status'])) { + $this->error_msg = Tools::displayError('Invalid booking status.'); + } elseif (!isset($params['room_bookings']) + || !count($params['room_bookings']) + ) { + $this->error_msg = Tools::displayError('Rooms not found in the request.'); + } elseif (isset($params['guest_detail']['id_customer']) + && !Validate::isLoadedObject(new Customer($params['guest_detail']['id_customer'])) + ) { + $this->error_msg = Tools::displayError('Invalid ID customer.'); + } elseif (!isset($params['id_property']) + || !Validate::isLoadedObject(new HotelBranchInformation((int) $params['id_property'])) + ) { + $this->error_msg = Tools::displayError('Please provide a valid id_property.'); + } elseif (!isset($params['guest_detail']['email']) + || !Validate::isEmail($params['guest_detail']['email']) + || !$objCustomer->getByEmail($params['guest_detail']['email']) + ) { + $this->error_msg = Tools::displayError('Customer not found.'); + } elseif (!$this->validateAddressFields($params['guest_detail']) + && $this->error_msg == '' + ) { + $this->error_msg = Tools::displayError('Invalid address provided.'); + } elseif (!$this->validatePutRequestRoomTypes($params['room_bookings'], $params['id_property']) + && $this->error_msg == '' + ) { + $this->error_msg = Tools::displayError('Requested room(s) not available'); + } + + if ($this->error_msg == '') { + return true; + } + + return false; + } + + /** + * Validating the PUT request Room types. + */ + public function validatePutRequestRoomTypes($roomTypes, $idHotel = false) + { + $objRoomType = new HotelRoomType(); + foreach ($roomTypes as $roomType) { + if ($this->validateRoomType($roomType)) { + // Validate that the room type belongs to the correct hotel + if ($idHotel) { + $roomTypeInfo = $objRoomType->getRoomTypeInfoByIdProduct($roomType['id_room_type']); + if ($idHotel != $roomTypeInfo['id_hotel']) { + $this->error_msg = Tools::displayError('Room type does not belong to the specified hotel.'); + return false; + } + } + + if (isset($roomType['rooms']) && count($roomType['rooms'])) { + foreach ($roomType['rooms'] as $room) { + if (!isset($room['id_room'])) { + if ($roomType['number_of_rooms']) { + $roomType['number_of_rooms']--; + } else { + return false; + } + } elseif (isset($room['id_room']) + && ( + !Validate::isLoadedObject($objHotelRoomInfomation = new HotelRoomInformation($room['id_room'])) + || $objHotelRoomInfomation->id_product != $roomType['id_room_type'] + ) + ) { + $this->error_msg = Tools::displayError('Invalid Id room.'); + return false; + } + + // Validate that the room belongs to the correct hotel + if (isset($room['id_room']) && isset($roomType['id_hotel'])) { + $objHotelRoom = new HotelRoomInformation($room['id_room']); + if (Validate::isLoadedObject($objHotelRoom) && $objHotelRoom->id_hotel != $roomType['id_hotel']) { + $this->error_msg = Tools::displayError('Room does not belong to the specified hotel.'); + return false; + } + } + + if (isset($room['id_tax_rules_group']) && !Validate::isLoadedObject(new TaxRulesGroup((int) $room['id_tax_rules_group']))) { + $this->error_msg = Tools::displayError('Invalid id_tax_rules_group.'); + return false; + } elseif (isset($room['total_tax']) && !Validate::isPrice($room['total_tax'])) { + $this->error_msg = Tools::displayError('Invalid total tax.'); + return false; + } + } + } + } else { + return false; + } + } + + return true; + } + + //process customer details to create bookings successfully + public function processCustomerDetails(array &$inputData) + { + $rawFirstname = (isset($inputData['guest_detail']['firstname']) && is_string($inputData['guest_detail']['firstname'])) + ? $inputData['guest_detail']['firstname'] + : ''; + $sanitizedFirstname = trim(preg_replace('/[0-9!<>,;?=+()@#"°{}_$%:]/u', '', stripslashes($rawFirstname))); + if ($sanitizedFirstname !== '' && mb_strlen($sanitizedFirstname) > 30) { + $sanitizedFirstname = mb_substr($sanitizedFirstname, 0, 30); + } + $inputData['guest_detail']['firstname'] = (Validate::isName($sanitizedFirstname) && $sanitizedFirstname !== '') ? $sanitizedFirstname : 'QloApps'; + + $rawLastname = (isset($inputData['guest_detail']['lastname']) && is_string($inputData['guest_detail']['lastname'])) + ? $inputData['guest_detail']['lastname'] + : ''; + $sanitizedLastname = trim(preg_replace('/[0-9!<>,;?=+()@#"°{}_$%:]/u', '', stripslashes($rawLastname))); + if ($sanitizedLastname !== '' && mb_strlen($sanitizedLastname) > 30) { + $sanitizedLastname = mb_substr($sanitizedLastname, 0, 30); + } + $inputData['guest_detail']['lastname'] = (Validate::isName($sanitizedLastname) && $sanitizedLastname !== '') ? $sanitizedLastname : $inputData['guest_detail']['firstname']; + + if(empty(trim($inputData['guest_detail']['phone'])) || !Validate::isPhoneNumber($inputData['guest_detail']['phone'])) { + $inputData['guest_detail']['phone'] = '12345678'; + } + + $guestEmail = (isset($inputData['guest_detail']['email']) && is_string($inputData['guest_detail']['email'])) + ? trim($inputData['guest_detail']['email']) + : ''; + if ($inputData['booking_status'] != self::API_BOOKING_STATUS_NEW && $inputData['id_booking']) { + if(Validate::isLoadedObject($objOrder = new Order((int)$inputData['id_booking']))) { + if(Validate::isLoadedObject($objCustomer = new Customer((int)$objOrder->id_customer))) { + $inputData['guest_detail']['email'] = $objCustomer->email; + } + } elseif (empty($guestEmail) || mb_strlen($guestEmail) > 128 || !Validate::isEmail($guestEmail)) { + // Unknown id_booking and unusable email: generate a placeholder. + $inputData['guest_detail']['email'] = Tools::strtolower(trim($inputData['guest_detail']['firstname'])); + $inputData['guest_detail']['email'] .= Tools::strtolower(trim($inputData['guest_detail']['lastname'])); + $inputData['guest_detail']['email'] .= Tools::passwdGen(3, 'NUMERIC').'@qloapps.com'; + } else { + // Unknown id_booking but valid email: keep it; manage() decides create or modify. + $inputData['guest_detail']['email'] = $guestEmail; + } + } else if (empty($guestEmail) || mb_strlen($guestEmail) > 128 || !Validate::isEmail($guestEmail)) { + $inputData['guest_detail']['email'] = Tools::strtolower(trim($inputData['guest_detail']['firstname'])); + $inputData['guest_detail']['email'] .= Tools::strtolower(trim($inputData['guest_detail']['lastname'])); + $inputData['guest_detail']['email'] .= Tools::passwdGen(3, 'NUMERIC').'@qloapps.com'; + } + + if (!isset($inputData['guest_detail']['city']) || empty(trim($inputData['guest_detail']['city'])) || !Validate::isCityName(trim($inputData['guest_detail']['city']))) { + $inputData['guest_detail']['city'] = 'Demo City'; + } + + if (!isset($inputData['guest_detail']['country_code']) || empty(trim($inputData['guest_detail']['country_code'])) || !preg_match('/^([A-Z]{2,3}|\d{3})$/i', trim($inputData['guest_detail']['country_code']))) { + if (Validate::isLoadedObject($objCountry = new Country((int)Configuration::get('PS_COUNTRY_DEFAULT')))) { + $inputData['guest_detail']['country_code'] = $objCountry->iso_code; + } else { + $inputData['guest_detail']['country_code'] = 'IN'; + } + } + + if ($idCountry = Country::getByIso($inputData['guest_detail']['country_code'])) { + if (Validate::isLoadedObject($objCountry = new Country($idCountry))) { + if ($objCountry->contains_states) { + if (!isset($inputData['guest_detail']['state']) || empty(trim($inputData['guest_detail']['state'])) || !Validate::isCleanHtml(trim($inputData['guest_detail']['state']))) { + if (($statesByCountry = State::getStatesByIdCountry($idCountry)) && isset($statesByCountry[0])) { + $inputData['guest_detail']['state'] = $statesByCountry[0]['name']; + } else { + $inputData['guest_detail']['state'] = 'Demo State'; + } + } + + if (!isset($inputData['guest_detail']['state_code']) || empty(trim($inputData['guest_detail']['state_code'])) || !Validate::isCleanHtml(trim($inputData['guest_detail']['state_code']))) { + if ($idState = State::getIdByName($inputData['guest_detail']['state'])) { + $inputData['guest_detail']['state_code'] = $idState; + } else if (($statesByCountry = State::getStatesByIdCountry($idCountry)) && isset($statesByCountry[0])) { + $inputData['guest_detail']['state_code'] = $statesByCountry[0]['id_state']; + } else { + $inputData['guest_detail']['state_code'] = 1; + } + } + } + + if($objCountry->need_zip_code) { + if(!isset($inputData['guest_detail']['zip']) || empty(trim($inputData['guest_detail']['zip'])) || !$objCountry->checkZipCode(trim($inputData['guest_detail']['zip']))){ + $inputData['guest_detail']['zip'] = Tools::generateRandomZipcode($objCountry->id); + } + } else { + $inputData['guest_detail']['zip'] = Tools::generateRandomZipcode($objCountry->id); + } + } + } + + if(!isset($inputData['guest_detail']['address']) || empty(trim($inputData['guest_detail']['address'])) || !Validate::isAddress(trim($inputData['guest_detail']['address']))) { + $addressParts = []; + + if (!empty($inputData['guest_detail']['city'])) { + $addressParts[] = $inputData['guest_detail']['city']; + } + + if (!empty($inputData['guest_detail']['state'])) { + $addressParts[] = $inputData['guest_detail']['state']; + } + + if (!empty($inputData['guest_detail']['zip'])) { + $addressParts[] = $inputData['guest_detail']['zip']; + } + + if (!empty($inputData['guest_detail']['country_code'])) { + $addressParts[] = $inputData['guest_detail']['country_code']; + } + + $inputData['guest_detail']['address'] = !empty($addressParts) ? implode(', ', $addressParts) : 'Demo Address'; + } + + // if invalid or empty address assign a demo address + if(empty($inputData['guest_detail']['address']) || !Validate::isAddress($inputData['guest_detail']['address'])) { + $inputData['guest_detail']['address'] = 'Demo Address'; + } + } + + /** + * Adding order payment from PUT requests. + */ + public function addOrderPayment($params) + { + if (isset($params['price_details']['total_price_with_tax']) && $params['price_details']['total_price_with_tax'] ) { + $objOrder = new Order($params['id_booking']); + // Target paid: the full total, or for partial only the received amount sent (none: skip). + $targetPaidAmount = (float) $params['price_details']['total_price_with_tax']; + if (isset($params['payment_status']) && $params['payment_status'] == self::API_BOOKING_PAYMENT_STATUS_PARTIAL) { + $targetPaidAmount = $this->getSentPaidAmount($params); + if ($targetPaidAmount === null) { + return; + } + $targetPaidAmount = min($targetPaidAmount, (float) $params['price_details']['total_price_with_tax']); + } + $amount = $targetPaidAmount - $objOrder->getTotalPaid(); + $paymentMethod = null; + if (isset($params['payment_detail']['payment_method']) && $params['payment_detail']['payment_method']) { + $paymentMethod = $params['payment_detail']['payment_method']; + } + + $transactionId = null; + if (isset($params['payment_detail']['transaction_id']) && $params['payment_detail']['transaction_id']) { + $transactionId = $params['payment_detail']['transaction_id']; + } + + // Same payment type mapping as on create. + $paymentType = OrderPayment::PAYMENT_TYPE_REMOTE_PAYMENT; + if (isset($params['payment_detail']['payment_type']) + && $params['payment_detail']['payment_type'] == 'pay at hotel' + ) { + $paymentType = OrderPayment::PAYMENT_TYPE_PAY_AT_HOTEL; + } + + $idCurrency = $objOrder->id_currency; + if (isset($params['currency']) + && ($selectedCurrency = Currency::getIdByIsoCode($params['currency'])) + ) { + $objCurrency = new Currency($selectedCurrency); + if ($objCurrency->active) { + $idCurrency = $selectedCurrency; + } + } + + $newCurrency = null; + if ($idCurrency != $objOrder->id_currency) { + $newCurrency = new Currency($idCurrency); + } + + $orderInvoice = null; + $idOrderInvoice = ($firstInvoice = $objOrder->getInvoicesCollection()->getFirst()) ? $firstInvoice->id : 0; + if ($idOrderInvoice) { + $orderInvoice = new OrderInvoice((int) $idOrderInvoice); + + $totalPaidTaxIncl = $params['price_details']['total_price_with_tax']; + $totalPaidTaxExcl = $totalPaidTaxIncl - $params['price_details']['total_tax']; + + $orderInvoice->total_paid_tax_excl = $totalPaidTaxExcl; + $orderInvoice->total_paid_tax_incl = $totalPaidTaxIncl; + // Products are pre-discount: take them from the order. + $orderInvoice->total_products = $objOrder->total_products; + $orderInvoice->total_products_wt = $objOrder->total_products_wt; + $orderInvoice->save(); + } + + if ($amount > 0) { + $objOrder->addOrderPayment( + $amount, + $paymentMethod, + $transactionId, + $newCurrency, + null, + $orderInvoice, + $paymentType + ); + } + $objOrder->total_paid_real = $targetPaidAmount; + if (isset($params['id_channel_manager_booking']) && $params['id_channel_manager_booking']) { + $objOrder->id_channel_manager_booking = $params['id_channel_manager_booking']; + } + $objOrder->save(); + $this->managePaymentHistory($objOrder->reference, $objOrder->total_paid_real); + + } + } + + /** + * Amount the request says was actually paid: price_details.total_paid, or + * payment_detail.amount_paid. Both are optional; the channel manager sends neither today. + * + * @param array $params booking_notification request + * + * @return float|null null when no usable amount was sent + */ + public function getSentPaidAmount($params) + { + foreach (array( + isset($params['price_details']['total_paid']) ? $params['price_details']['total_paid'] : null, + isset($params['payment_detail']['amount_paid']) ? $params['payment_detail']['amount_paid'] : null, + ) as $amount) { + if ($amount !== null && $amount !== '' && Validate::isPrice($amount)) { + return Tools::ps_round((float) $amount, _PS_PRICE_COMPUTE_PRECISION_); + } + } + + return null; + } + + /** + * Trims the payments recorded for an order reference down to the paid amount sent by the CM. + * + * Compares the sum of payments and removes the excess, newest first (e.g. core's "rest paid" + * payment from validateOrder()). A shortfall is left to addOrderPayment(). + * + * @param string $orderReference + * @param float $totalPaidAmount + * + * @return void + */ + public function managePaymentHistory($orderReference, $totalPaidAmount) + { + if ($orderReference && $totalPaidAmount) { + $getSql = 'SELECT `id_order_payment`, `amount` + FROM `' . _DB_PREFIX_ . 'order_payment` + WHERE `order_reference` = "' . pSQL($orderReference) . '" + ORDER BY `id_order_payment` DESC'; + + if ($results = Db::getInstance()->executeS($getSql)) { + $recordedAmount = 0; + foreach ($results as $result) { + $recordedAmount += (float) $result['amount']; + } + + $excessAmount = Tools::ps_round($recordedAmount - (float) $totalPaidAmount, _PS_PRICE_COMPUTE_PRECISION_); + foreach ($results as $result) { + if ($excessAmount <= 0) { + break; + } + + $objPaymentHistory = new OrderPayment((int) $result['id_order_payment']); + if (!Validate::isLoadedObject($objPaymentHistory)) { + continue; + } + + if ((float) $objPaymentHistory->amount <= $excessAmount) { + // Whole payment is excess: delete it with its detail and invoice links. + $excessAmount = Tools::ps_round($excessAmount - (float) $objPaymentHistory->amount, _PS_PRICE_COMPUTE_PRECISION_); + Db::getInstance()->delete('order_invoice_payment', '`id_order_payment` = '.(int) $objPaymentHistory->id); + Db::getInstance()->delete('order_payment_detail', '`id_order_payment` = '.(int) $objPaymentHistory->id); + $objPaymentHistory->delete(); + } else { + $objPaymentHistory->amount = Tools::ps_round((float) $objPaymentHistory->amount - $excessAmount, _PS_PRICE_COMPUTE_PRECISION_); + $objPaymentHistory->save(); + if ($idPaymentDetail = (int) Db::getInstance()->getValue( + 'SELECT `id_order_payment_detail` FROM `'._DB_PREFIX_.'order_payment_detail` + WHERE `id_order_payment` = '.(int) $objPaymentHistory->id.' + ORDER BY `id_order_payment_detail` DESC' + )) { + $objPaymentDetail = new OrderPaymentDetail($idPaymentDetail); + if (Validate::isLoadedObject($objPaymentDetail)) { + $objPaymentDetail->amount = max(0, Tools::ps_round((float) $objPaymentDetail->amount - $excessAmount, _PS_PRICE_COMPUTE_PRECISION_)); + $objPaymentDetail->save(); + } + } + $excessAmount = 0; + } + } + } + } + } + + public function handleRoomTypePriceWithService(array &$inputData) + { + // No-op: the CM sends room prices net of services, which are their own lines. + return; + } + + public function checkForPariallyAvailableRooms($bookingParams, $idCart, $idOrder=false) + { + $objBookingDetail = new HotelBookingDetail(); + $isPartiallyAvailable = false; + $isAvailable = false; + $params = array( + 'date_from' => $bookingParams['date_from'], + 'date_to' => $bookingParams['date_to'], + 'hotel_id' => $bookingParams['hotel_id'], + 'id_room_type' => $bookingParams['id_room_type'], + 'num_rooms' => $bookingParams['number_of_rooms'], + 'search_available' => isset($bookingParams['search_available']) ? $bookingParams['search_available'] : 0, + 'search_partial' => 1, + 'search_booked' => 0, + 'search_unavai' => 0, + 'id_cart' => $idCart ? $idCart : $this->context->cart->id, + 'id_guest' => null, + 'search_cart_rms' => 0, + 'occupancy' => $bookingParams['occupancy'], + 'id_order' => $idOrder + ); + + $roomTypeAvailability = $objBookingDetail->getBookingData($params); + if($roomTypeAvailability['stats']) { + if(isset($roomTypeAvailability['stats']['num_part_avai'])) { + $isPartiallyAvailable = $roomTypeAvailability['stats']['num_part_avai'] ? true : false; + } + if(isset($roomTypeAvailability['stats']['num_avail'])) { + $isAvailable = $roomTypeAvailability['stats']['num_avail'] ? true : false; + } + } + return $isPartiallyAvailable || $isAvailable; + } + + function getFreeSegments($reqFrom, $reqTo, $bookFrom, $bookTo) + { + $segments = []; + + if ($reqFrom < $bookFrom) { + $beforeEnd = min($reqTo, $bookFrom); + + if ($beforeEnd > $reqFrom) { + $segments[] = [ + 'date_from' => $reqFrom, + 'date_to' => $beforeEnd + ]; + } + } + + if ($reqTo > $bookTo) { + $afterStart = max($reqFrom, $bookTo); + + if ($reqTo > $afterStart) { + $segments[] = [ + 'date_from' => $afterStart, + 'date_to' => $reqTo + ]; + } + } + + return $segments; + } + + /** + * How far a specific physical room stays contiguously free, scanning night by + * night from $anchorDate toward $limitDate (limit may be later or earlier than + * anchor, supporting both tail- and head-extensions). Delegates to the core + * dataForFrontSearch() per night so real bookings, disable-dates and LOS + * restrictions are all honored the same way the rest of the system honors them. + * + * Returns $anchorDate unchanged if blocked on the very first night, $limitDate + * if every night is free, or the boundary in between. + */ + public function findSameRoomAvailableUntil($idRoom, $idRoomType, $idHotel, $idOrder, $anchorDate, $limitDate) + { + $anchorTs = strtotime(date('Y-m-d', strtotime($anchorDate))); + $limitTs = strtotime(date('Y-m-d', strtotime($limitDate))); + if ($limitTs == $anchorTs) { + return date('Y-m-d', $anchorTs); + } + + $forward = $limitTs > $anchorTs; + $step = $forward ? '+1 day' : '-1 day'; + $objBookingDetail = new HotelBookingDetail(); + $cursor = $anchorTs; + + while ($cursor != $limitTs) { + $next = strtotime($step, $cursor); + $dateFrom = date('Y-m-d', min($cursor, $next)); + $dateTo = date('Y-m-d', max($cursor, $next)); + + $data = $objBookingDetail->dataForFrontSearch(array( + 'date_from' => $dateFrom, + 'date_to' => $dateTo, + 'hotel_id' => $idHotel, + 'id_room_type' => $idRoomType, + 'only_search_data' => 1, + )); + + $roomFreeThisNight = isset($data['rm_data'][$idRoomType]['data']['available'][$idRoom]); + if (!$roomFreeThisNight) { + return date('Y-m-d', $cursor); + } + $cursor = $next; + } + + return date('Y-m-d', $limitTs); + } + + /** + * Pure validation + in-memory resolution pass for handlePutRequest(). Must run + * AFTER $roomsToUpdate/$roomsToAdd are fully built and validateRequestedRoomTypes() + * has passed, and BEFORE addRoomsInOrder()/the roomsToUpdate mutation loop run. + * Makes zero HotelBookingDetail/OrderDetail/Order writes itself. + * + * For every already-assigned room whose requested date range is a genuine + * extension (per getFreeSegments()), determines how far the SAME physical room + * can cover the extension via findSameRoomAvailableUntil(). Caps that room's own + * update to what it can safely cover, and for any uncoverable leftover nights, + * synthesizes a $roomsToAdd entry (room type, 1 room, no id_room preference) so + * the existing addRoomsInOrder()/addRoomsInCart() pipeline performs the real + * availability search + rejection for the leftover. Pure shrinks (no genuine + * extension on either side) are left untouched — always safe, no check needed. + */ + public function resolveRoomExtensionAvailability(&$roomsToUpdate, &$roomsToAdd, $idOrder) + { + if (!$roomsToUpdate) { + return; + } + + foreach ($roomsToUpdate as &$roomsByDate) { + if (empty($roomsByDate['requested'])) { + continue; + } + + foreach ($roomsByDate['requested'] as $roomKey => &$room) { + if (!isset($roomsByDate['order'][$roomKey])) { + continue; + } + $orderRow = $roomsByDate['order'][$roomKey]; + $idRoom = (int) $orderRow['id_room']; + $idRoomType = (int) $orderRow['id_product']; + $idHotel = (int) $orderRow['id_hotel']; + + $oldFrom = date('Y-m-d', strtotime($orderRow['date_from'])); + $oldTo = date('Y-m-d', strtotime($orderRow['date_to'])); + $newFrom = date('Y-m-d', strtotime($room['check_in_date'])); + $newTo = date('Y-m-d', strtotime($room['check_out_date'])); + + $deltaSegments = $this->getFreeSegments($newFrom, $newTo, $oldFrom, $oldTo); + if (!$deltaSegments) { + continue; + } + + $requestedPriceTaxExcl = isset($room['unit_price_without_tax']) + ? $room['unit_price_without_tax'] + : (isset($room['total_price_with_tax'], $room['total_tax']) + ? $room['total_price_with_tax'] - $room['total_tax'] : 0); + $requestedTax = isset($room['total_tax']) ? floatval($room['total_tax']) : 0; + $requestedNights = HotelHelper::getNumberOfDays($newFrom, $newTo) ?: 1; + + $cappedFrom = $newFrom; + $cappedTo = $newTo; + + foreach ($deltaSegments as $segment) { + $isHeadDelta = strtotime($segment['date_from']) < strtotime($oldFrom); + $anchorDate = $isHeadDelta ? $oldFrom : $oldTo; + $limitDate = $isHeadDelta ? $segment['date_from'] : $segment['date_to']; + + $boundary = $this->findSameRoomAvailableUntil( + $idRoom, $idRoomType, $idHotel, $idOrder, $anchorDate, $limitDate + ); + + if ($isHeadDelta) { + $cappedFrom = $boundary; + $leftoverFrom = $segment['date_from']; + $leftoverTo = $boundary; + } else { + $cappedTo = $boundary; + $leftoverFrom = $boundary; + $leftoverTo = $segment['date_to']; + } + + if (strtotime($leftoverFrom) < strtotime($leftoverTo)) { + $leftoverNights = HotelHelper::getNumberOfDays($leftoverFrom, $leftoverTo) ?: 1; + $leftoverPriceTaxExcl = Tools::ps_round( + ($requestedPriceTaxExcl / $requestedNights) * $leftoverNights, + _PS_PRICE_COMPUTE_PRECISION_ + ); + $leftoverTaxAmt = Tools::ps_round( + ($requestedTax / $requestedNights) * $leftoverNights, + _PS_PRICE_COMPUTE_PRECISION_ + ); + + $splitKey = $idRoomType.'_'.strtotime($leftoverFrom).strtotime($leftoverTo).'_split_'.$idRoom.'_'.$roomKey; + $roomsToAdd[$splitKey] = array( + 'id_room_type' => $idRoomType, + 'number_of_rooms' => 1, + 'check_in_date' => $leftoverFrom, + 'check_out_date' => $leftoverTo, + 'rooms' => array( + 'r_0' => array( + 'occupancy' => isset($room['occupancy']) ? $room['occupancy'] : array(), + 'services' => array(), + 'unit_price_without_tax' => $leftoverPriceTaxExcl, + 'total_price_with_tax' => $leftoverPriceTaxExcl + $leftoverTaxAmt, + 'total_tax' => $leftoverTaxAmt, + 'taxes' => isset($room['taxes']) ? $room['taxes'] : false, + ), + ), + ); + } + } + + if ($cappedFrom !== $newFrom || $cappedTo !== $newTo) { + $cappedNights = HotelHelper::getNumberOfDays($cappedFrom, $cappedTo) ?: 1; + $cappedPriceTaxExcl = Tools::ps_round( + ($requestedPriceTaxExcl / $requestedNights) * $cappedNights, + _PS_PRICE_COMPUTE_PRECISION_ + ); + $cappedTaxAmt = Tools::ps_round( + ($requestedTax / $requestedNights) * $cappedNights, + _PS_PRICE_COMPUTE_PRECISION_ + ); + + $room['check_in_date'] = $cappedFrom; + $room['check_out_date'] = $cappedTo; + $room['date_from'] = $cappedFrom; + $room['date_to'] = $cappedTo; + $room['unit_price_without_tax'] = $cappedPriceTaxExcl; + $room['total_price_with_tax'] = $cappedPriceTaxExcl + $cappedTaxAmt; + $room['total_tax'] = $cappedTaxAmt; + } + } + unset($room); + } + unset($roomsByDate); + } +} diff --git a/modules/qlochannelmanagerconnector/controllers/admin/index.php b/modules/qlocmconnector/classes/index.php similarity index 100% rename from modules/qlochannelmanagerconnector/controllers/admin/index.php rename to modules/qlocmconnector/classes/index.php diff --git a/modules/qlochannelmanagerconnector/controllers/admin/AdminQloappsChannelManagerConnectorController.php b/modules/qlocmconnector/controllers/admin/AdminQloappsChannelManagerConnectorController.php similarity index 54% rename from modules/qlochannelmanagerconnector/controllers/admin/AdminQloappsChannelManagerConnectorController.php rename to modules/qlocmconnector/controllers/admin/AdminQloappsChannelManagerConnectorController.php index 3ad221cc6..cfa19e95a 100644 --- a/modules/qlochannelmanagerconnector/controllers/admin/AdminQloappsChannelManagerConnectorController.php +++ b/modules/qlocmconnector/controllers/admin/AdminQloappsChannelManagerConnectorController.php @@ -25,105 +25,110 @@ class AdminQloappsChannelManagerConnectorController extends ModuleAdminControlle { public function __construct() { - $this->table = 'qcmc_channel_manager_booking'; - $this->className = 'QcmcChannelManagerBooking'; + $this->table = 'order'; + $this->className = 'Order'; + $this->identifier = 'id_order'; + $this->lang = false; + $this->deleted = false; $this->bootstrap = true; $this->toolbar_title = $this->l('Channel Manager Bookings'); $this->context = Context::getContext(); - $this->identifier = 'id_channel_manager_booking'; parent::__construct(); - $this->_join .= ' INNER JOIN `'._DB_PREFIX_.'orders` ord ON (a.id_order = ord.`id_order`)'; - - $this->_select .= ' ord.`source`, ord.`total_paid`, ord.`total_paid_real`, IF(a.id_order, 1, 0) badge_success'; + // 'a' is the orders row itself now (no separate booking table) — an order is CM-sourced + // if id_channel_manager_booking is set, OR its source isn't the shop's own domain (covers + // CM bookings where the channel manager never sent id_channel_manager_booking). + $this->_where = ' AND ( ( a.`id_channel_manager_booking` IS NOT NULL AND a.`id_channel_manager_booking` != \'\' ) + OR a.`source` != \''.pSQL(Configuration::get('PS_SHOP_DOMAIN')).'\' ) '; + $this->_orderBy = 'date_add'; $this->_orderWay = 'DESC'; $this->fields_list = array( 'id_order' => array( 'title' => $this->l('Id order'), - 'align' => 'center', - 'havingFilter' => true, + 'align' => 'text-center', + 'class' => 'fixed-width-xs', + 'filter_key' => 'a!id_order', 'callback' => 'getOrderLink', ), 'source' => array( 'title' => $this->l('Channel'), - 'align' => 'center', + 'align' => 'text-center', + 'filter_key' => 'a!source', ), 'total_paid' => array( 'title' => $this->l('Order total'), - 'align' => 'center', + 'align' => 'text-center', + 'type' => 'range', + 'validation' => 'isFloat', + 'filter_key' => 'a!total_paid', 'callback' => 'setPriceCurrency', - 'badge_success' => true, ), 'total_paid_real' => array( 'title' => $this->l('Recieved amount'), - 'align' => 'center', + 'align' => 'text-center', + 'type' => 'range', + 'validation' => 'isFloat', + 'filter_key' => 'a!total_paid_real', 'callback' => 'setPriceCurrencyWithBadge', ), 'date_add' => array( 'title' => $this->l('Created on'), - 'align' => 'center', + 'align' => 'text-center', + 'type' => 'datetime', + 'filter_key' => 'a!date_add', ), ); $this->list_no_link = true; } - public function getOrderLink($id_order, $row) + public function getOrderLink($idOrder, $row) { $displayData = ''; - if ($id_order) { - $displayData .= '#'.$id_order; - // $displayData .= '#'.$id_order.''; + if ($idOrder) { + $displayData .= '#'.$idOrder; } return $displayData; } public function setPriceCurrency($value, $row) { - if (Validate::isLoadedObject($objOrder = new Order($row['id_order']))) { - return Tools::displayPrice($value, (int)$objOrder->id_currency); - } + return Tools::displayPrice($value, (int)$row['id_currency']); } public function setPriceCurrencyWithBadge($value, $row) { $displayData = ''; - if (Validate::isLoadedObject($objOrder = new Order($row['id_order']))) { - $displayData .= ''; - $displayData .= Tools::displayPrice($value, (int)$objOrder->id_currency); - $displayData .= ''; - } + $displayData .= ''; + $displayData .= Tools::displayPrice($value, (int)$row['id_currency']); + $displayData .= ''; return $displayData; } public function displayViewLink($token, $idRow, $name = null) { - if (Validate::isLoadedObject($objChannelManagerBooking = new QcmcChannelManagerBooking($idRow))) { - return ' '.$this->l('View Order Detail').''; - } + return ' '.$this->l('View Order Detail').''; } public function renderList() { - if ($channelManagerBookings = QcmcChannelManagerBooking::getChannelManagerBookings(0, 'DESC')) { + if (QcmcChannelManagerOrder::isChannelManagerConnected()) { $this->context->smarty->assign( array ( - 'icon' => 'icon-list', 'toolbar_title' => 'icon-list', - // As we get the bookings in descending order according to the date_add. So in the 0 index last booking will be found - 'last_booking_datetime' => Tools::displayDate($channelManagerBookings[0]['date_add'], null, true), + 'last_booking_datetime' => Tools::displayDate(QcmcChannelManagerOrder::getLastChannelManagerBookingDate(), null, true), + 'is_cm_connected' => QcmcChannelManagerOrder::isChannelManagerConnected(), ) ); // because in helper list tpl_vars is given priority and we need different List title than list title // In HelperList.php assigned: 'title' => array_key_exists('title', $this->tpl_vars) ? $this->tpl_vars['title'] : $this->title - $this->tpl_list_vars['title'] = $this->l('Below is the list of all the bookings created by channel manager.'); + $this->tpl_list_vars['title'] = $this->l('Channel Manager Bookings'); unset($this->toolbar_btn['new']); @@ -135,11 +140,12 @@ class AdminQloappsChannelManagerConnectorController extends ModuleAdminControlle array ( 'module_dir' => _MODULE_DIR_, 'current_datetime' => Tools::displayDate(date('Y-m-d H:i:s'), null, true), + 'is_cm_connected' => QcmcChannelManagerOrder::isChannelManagerConnected(), ) ); $this->content .= $this->context->smarty->fetch( _PS_MODULE_DIR_. - 'qlochannelmanagerconnector/views/templates/admin/qloapps_channel_manager_connector/channel_manager_connect_info.tpl' + 'qlocmconnector/views/templates/admin/qloapps_channel_manager_connector/qcmc_channel_manager_connect_info.tpl' ); } } @@ -148,11 +154,10 @@ class AdminQloappsChannelManagerConnectorController extends ModuleAdminControlle { parent::setMedia(); - if (QcmcChannelManagerBooking::getChannelManagerBookings()) { - $this->addCSS(_MODULE_DIR_.'qlochannelmanagerconnector/views/css/admin/wk_cm_booking_list.css'); - $this->addJS(_MODULE_DIR_.'qlochannelmanagerconnector/views/js/admin/wk_cm_booking_list.js'); + if (QcmcChannelManagerOrder::isChannelManagerConnected()) { + $this->addJS(_MODULE_DIR_.'qlocmconnector/views/js/admin/qcmc_cm_booking_list.js'); } else { - $this->addCSS(_MODULE_DIR_.'qlochannelmanagerconnector/views/css/admin/wk_cm_info.css'); + $this->addCSS(_MODULE_DIR_.'qlocmconnector/views/css/admin/qcmc_cm_info.css'); } } } diff --git a/modules/qlochannelmanagerconnector/index.php b/modules/qlocmconnector/controllers/admin/index.php similarity index 100% rename from modules/qlochannelmanagerconnector/index.php rename to modules/qlocmconnector/controllers/admin/index.php diff --git a/modules/qlochannelmanagerconnector/views/css/admin/index.php b/modules/qlocmconnector/controllers/index.php similarity index 100% rename from modules/qlochannelmanagerconnector/views/css/admin/index.php rename to modules/qlocmconnector/controllers/index.php diff --git a/modules/qlochannelmanagerconnector/views/css/hook/index.php b/modules/qlocmconnector/index.php similarity index 100% rename from modules/qlochannelmanagerconnector/views/css/hook/index.php rename to modules/qlocmconnector/index.php diff --git a/modules/qlochannelmanagerconnector/logo.gif b/modules/qlocmconnector/logo.gif similarity index 100% rename from modules/qlochannelmanagerconnector/logo.gif rename to modules/qlocmconnector/logo.gif diff --git a/modules/qlochannelmanagerconnector/logo.png b/modules/qlocmconnector/logo.png similarity index 100% rename from modules/qlochannelmanagerconnector/logo.png rename to modules/qlocmconnector/logo.png diff --git a/modules/qlocmconnector/qlochannelmanagerapicron.php b/modules/qlocmconnector/qlochannelmanagerapicron.php new file mode 100644 index 000000000..310ed64e0 --- /dev/null +++ b/modules/qlocmconnector/qlochannelmanagerapicron.php @@ -0,0 +1,103 @@ +secure_key)) { + qcmc_log("Failed to get or refresh access token. Aborting cron."); + die('Something went wrong.'); +} + + +try { + $clientId = Configuration::get('QCMC_CM_CLIENT_ID'); + $clientSecret = Configuration::get('QCMC_CM_CLIENT_SECRET'); + + if($clientId && $clientSecret) { + if(Configuration::get('QCMC_CM_ACCESS_TOKEN')) { + if(!$objAri->processAriUpdateData()) { + $result = $objChannelManagerApiService->getAccessToken($clientId, $clientSecret); + if(isset($result['access_token'])) { + if($objChannelManagerApiService->updateAccessToken($result['access_token'])) { + $objAri->processAriUpdateData(); + } + } else { + qcmc_log("Error getting access token"); + } + } + } else { + $result = $objChannelManagerApiService->getAccessToken($clientId, $clientSecret); + if(isset($result['access_token'])) { + if($objChannelManagerApiService->updateAccessToken($result['access_token'])) { + $objAri->processAriUpdateData(); + } + } else { + qcmc_log("Error getting access token"); + } + } + } +} catch (Exception $e) { + qcmc_log("Fatal error in cron: " . $e->getMessage()); +} + +echo "Process Completed successsfully"; + +if (!file_exists($markerFile) || ((time() - (int)@file_get_contents($markerFile)) >= 86400)) { + foreach (glob($logDir . "/*.log") as $file) { + if (preg_match('/^\d{4}-\d{2}-\d{2}$/', basename($file, '.log'))) { + $fileDate = strtotime(basename($file, '.log')); + if ($fileDate !== false && $fileDate < strtotime('-60 days')) { + @unlink($file); + } + } + } + @file_put_contents($markerFile, time()); +} +die; diff --git a/modules/qlocmconnector/qlocmconnector.php b/modules/qlocmconnector/qlocmconnector.php new file mode 100644 index 000000000..e46fe5851 --- /dev/null +++ b/modules/qlocmconnector/qlocmconnector.php @@ -0,0 +1,1342 @@ +name = 'qlocmconnector'; + $this->tab = 'analytics_stats'; + $this->version = '7.0.0'; + $this->author = 'Webkul'; + $this->bootstrap = true; + $this->secure_key = Tools::encrypt($this->name); + $this->html = ''; + $this->ps_versions_compliancy = array('min' => '1.6', 'max' => '1.6'); + $this->qloapps_versions_compliancy = array('min' => '1.7', 'max' => _QLOAPPS_VERSION_); + parent::__construct(); + + $this->displayName = $this->l('QloApps Channel Manager Connector'); + $this->description = $this->l('This module connects the PMS with Channel Manager using a simplified API endpoint.'); + $this->confirmUninstall = $this->l('Are you sure you want to uninstall?'); + } + + public function getContent() + { + if (Tools::isSubmit('submitQcmcConfigForm')) { + if ($this->validateConfigurationValues()) { + if($this->saveConfigurationValues()) { + Tools::redirectAdmin( + $this->context->link->getAdminLink('AdminModules', true) + . '&conf=6&configure=' . $this->name + ); + } + } + } + + $this->html .= $this->renderForm(); + $this->html .= $this->renderCronInfo(); + + return $this->html; + } + + public function validateConfigurationValues() + { + if (empty(trim(Tools::getValue('QCMC_CM_CLIENT_ID')))) { + $this->context->controller->errors[] = + $this->l('Please enter Channel Manager API Client ID.'); + } + + if (empty(trim(Tools::getValue('QCMC_CM_CLIENT_SECRET')))) { + $this->context->controller->errors[] = + $this->l('Please enter Channel Manager API Client Secret.'); + } + + if (!Validate::isCleanHtml(Tools::getValue('QCMC_CM_CLIENT_ID'))) { + $this->context->controller->errors[] = + $this->l('Invalid Client ID.'); + } + + if (!Validate::isCleanHtml(Tools::getValue('QCMC_CM_CLIENT_SECRET'))) { + $this->context->controller->errors[] = + $this->l('Invalid Client Secret.'); + } + + return empty($this->context->controller->errors); + } + + public function saveConfigurationValues() + { + $newClientId = trim(Tools::getValue('QCMC_CM_CLIENT_ID')); + $newClientSecret = trim(Tools::getValue('QCMC_CM_CLIENT_SECRET')); + + $oldClientId = Configuration::get('QCMC_CM_CLIENT_ID'); + $oldClientSecret = Configuration::get('QCMC_CM_CLIENT_SECRET'); + + if ($newClientId !== $oldClientId || $newClientSecret !== $oldClientSecret) { + $objQcmcChannelManagerApiService = new QcmcChannelManagerApiService(); + $result = $objQcmcChannelManagerApiService->getAccessToken($newClientId, $newClientSecret); + if(!empty($result['access_token'])) { + if($objQcmcChannelManagerApiService->updateAccessToken($result['access_token'])) { + Configuration::updateValue('QCMC_CM_CLIENT_ID', $newClientId); + Configuration::updateValue('QCMC_CM_CLIENT_SECRET', $newClientSecret); + return true; + } + } else { + $this->context->controller->errors[] = + $this->l('Wrong credentials used.'); + + return false; + } + + } + return true; + } + + public function renderForm() + { + $helper = new HelperForm(); + $helper->show_toolbar = false; + $helper->table = $this->table; + $helper->module = $this; + $helper->default_form_language = $this->context->language->id; + $helper->identifier = $this->identifier; + $helper->submit_action = 'submitQcmcConfigForm'; + $helper->currentIndex = + $this->context->link->getAdminLink('AdminModules', false) + . '&configure=' . $this->name + . '&tab_module=' . $this->tab + . '&module_name=' . $this->name; + + $helper->token = Tools::getAdminTokenLite('AdminModules'); + $helper->tpl_vars = array( + 'fields_value' => $this->getConfigFormFieldsValue(), + 'languages' => $this->context->controller->getLanguages(), + 'id_language' => $this->context->language->id, + ); + + return $helper->generateForm(array($this->getConfigFormFields())); + } + + public function getConfigFormFieldsValue() + { + $configKeys = array( + 'QCMC_CM_CLIENT_ID', + 'QCMC_CM_CLIENT_SECRET' + ); + $fieldsValue = array(); + foreach ($configKeys as $key) { + $fieldsValue[$key] = Configuration::get($key) ? Configuration::get($key) : Tools::getValue($key); + } + + return $fieldsValue; + } + + public function getConfigFormFields() + { + return array( + 'form' => array( + 'legend' => array( + 'title' => $this->l('Channel Manager API Credentials'), + 'icon' => 'icon-cogs', + ), + 'description' => ' + '.$this->l('How to get Channel Manager API Credentials').' +
    +
  1. '.$this->l('Log in to your').''.$this->l(' Channel Manager Account.').'
  2. +
  3. '.$this->l('Navigate to Account Settings.').'
  4. +
  5. '.$this->l('Generate your').''. $this->l(' API Credentials.').'
  6. +
  7. '.$this->l('Enter your credentials into the fields below.').'
  8. +
+ ', + 'input' => array( + array( + 'type' => 'text', + 'name' => 'QCMC_CM_CLIENT_ID', + 'label' => $this->l('API Client ID'), + 'hint' => $this->l('Client ID from your Channel Manager account API credentials'), + 'required' => true, + 'col' => '4', + ), + array( + 'type' => 'text', + 'name' => 'QCMC_CM_CLIENT_SECRET', + 'label' => $this->l('API Client Secret'), + 'hint' => $this->l('Client Secret from your Channel Manager account API credentials'), + 'required' => true, + 'col' => '4', + ), + ), + 'submit' => array( + 'title' => $this->l('Save'), + ), + ), + ); + } + + public function renderCronInfo() + { + $cronUrl = Tools::getShopDomainSsl(true, true).__PS_BASE_URI__. + 'modules/qlocmconnector/qlochannelmanagerapicron.php?token='.$this->secure_key; + + $this->context->smarty->assign(array( + 'cron_url' => $cronUrl, + 'curl_missing' => !extension_loaded('curl'), + )); + + return $this->display(__FILE__, 'views/templates/admin/cron_info.tpl'); + } + + public function hookAddWebserviceResources() + { + $resources = array( + 'cm_api' => array( + 'description' => 'Channel Manager Connector API', + 'specific_management' => true + ), + ); + + return $resources; + } + + //Handles Ari update when room reallocate to new room type + public function hookActionRoomReallocateAfter($params) + { + $objAriUpdates = new QcmcAriUpdates(); + $idHotelBookingTo = (int)(isset($params['id_htl_booking_to']) ? $params['id_htl_booking_to'] : 0); + $objectHotelBookingFrom = isset($params['objectHotelBookingFrom']) ? $params['objectHotelBookingFrom'] : NULL; + + if(Validate::isLoadedObject($objectHotelBookingFrom)) { + if (Validate::isLoadedObject($objOrder = new Order((int)$objectHotelBookingFrom->id_order))) { + if($objOrder->id_channel_manager_booking == NULL && ($objOrder->source == Configuration::get('PS_SHOP_DOMAIN'))) return; + } + + $objAriUpdates->saveAriUpdateRow( + (int)$objectHotelBookingFrom->id_hotel, + (int)$objectHotelBookingFrom->id_product, + $objectHotelBookingFrom->date_from, + $objectHotelBookingFrom->date_to + ); + } + + if (Validate::isLoadedObject($objectHotelBookingTo = new HotelBookingDetail($idHotelBookingTo))) { + $objAriUpdates->saveAriUpdateRow( + (int)$objectHotelBookingTo->id_hotel, + (int)$objectHotelBookingTo->id_product, + $objectHotelBookingTo->date_from, + $objectHotelBookingTo->date_to + ); + } + } + + //Handles Ari update when any room of a room type is temp. disable for given range + public function hookActionObjectHotelRoomDisableDatesAddAfter($params) + { + if (isset($params['object']) && ($params['object'] instanceof HotelRoomDisableDates)) { + $objAriUpdates = new QcmcAriUpdates(); + $objHotelRoomInfo = new HotelRoomInformation(); + + $objDisableDates = $params['object']; + $roomInfoArray = $objHotelRoomInfo->getHotelRoomInfoByProductId((int)$objDisableDates->id_room_type); + + $objAriUpdates->saveAriUpdateRow( + (int)$roomInfoArray[0]['id_hotel'], + (int)$objDisableDates->id_room_type, + date('Y-m-d', strtotime($objDisableDates->date_from)), + date('Y-m-d', strtotime($objDisableDates->date_to)) + ); + } + } + + //Handles Ari update when temp. disable for given range is removed from any room type + public function hookActionObjectHotelRoomDisableDatesDeleteAfter($params) + { + if (isset($params['object']) && ($params['object'] instanceof HotelRoomDisableDates)) { + $objAriUpdates = new QcmcAriUpdates(); + $objHotelRoomInfo = new HotelRoomInformation(); + + $objDisableDatesDelete = $params['object']; + + $roomInfoArray = $objHotelRoomInfo->getHotelRoomInfoByProductId((int)$objDisableDatesDelete->id_room_type); + + $objAriUpdates->saveAriUpdateRow( + (int)$roomInfoArray[0]['id_hotel'], + (int)$objDisableDatesDelete->id_room_type, + pSQL($objDisableDatesDelete->date_from), + pSQL($objDisableDatesDelete->date_to) + ); + } + } + + //Handles Ari update when any room of a room type is set active/inactive + public function hookActionValidateRoomInformation($params) + { + if(isset($params['room_information'])) { + $newRoomInfo = $params['room_information']; + + if(isset($newRoomInfo['id'])) { + $idRoomInfo = (int)$newRoomInfo['id']; + + $objAriUpdates = new QcmcAriUpdates(); + $objHotelRoomInfo = new HotelRoomInformation(); + + $oldRoom = new HotelRoomInformation($idRoomInfo); + $oldStatus = (int)$oldRoom->id_status; + $newStatus = isset($newRoomInfo['id_status']) ? (int)$newRoomInfo['id_status'] : $oldStatus; + + if (($newStatus !== HotelRoomInformation::STATUS_TEMPORARY_INACTIVE) && ($oldStatus !== $newStatus)) { + $roomInfoArray = $objHotelRoomInfo->getHotelRoomInfoByProductId((int)$oldRoom->id_product); + $maxBookingOffset = HotelOrderRestrictDate::getMaxOrderDate((int)$roomInfoArray[0]['id_hotel']); + + $objAriUpdates->saveAriUpdateRow( + (int)$roomInfoArray[0]['id_hotel'], + (int)$oldRoom->id_product, + date('Y-m-d'), + $maxBookingOffset + ); + } + } + } + } + + //Handles Ari update when a new room is added in any room type + public function hookActionObjectHotelRoomInformationAddAfter($params) + { + if (isset($params['object']) && ($params['object'] instanceof HotelRoomInformation)) { + $objAriUpdates = new QcmcAriUpdates(); + + $objHotelRoomInfoAdd = $params['object']; + + $maxBookingOffset = HotelOrderRestrictDate::getMaxOrderDate((int)$objHotelRoomInfoAdd->id_hotel); + + $objAriUpdates->saveAriUpdateRow( + (int)$objHotelRoomInfoAdd->id_hotel, + (int)$objHotelRoomInfoAdd->id_product, + date('Y-m-d'), + $maxBookingOffset + ); + } + } + + //Handles Ari update when a room is deleted from any room type + public function hookActionObjectHotelRoomInformationDeleteAfter($params) + { + if (isset($params['object']) && ($params['object'] instanceof HotelRoomInformation)) { + $objAriUpdates = new QcmcAriUpdates(); + + $objHotelRoomInfoDelete = $params['object']; + + $maxBookingOffset = HotelOrderRestrictDate::getMaxOrderDate((int)$objHotelRoomInfoDelete->id_hotel); + + $objAriUpdates->saveAriUpdateRow( + (int)$objHotelRoomInfoDelete->id_hotel, + (int)$objHotelRoomInfoDelete->id_product, + date('Y-m-d'), + $maxBookingOffset + ); + } + } + + //Handles Ari update when an order is updated for any field which affects the availablity + public function hookActionObjectHotelBookingDetailUpdateBefore($params) + { + if (isset($params['object']) && ($params['object'] instanceof HotelBookingDetail)) { + + $objHotelBookingDetail = $params['object']; + if (Validate::isLoadedObject($objOrder = new Order((int)$objHotelBookingDetail->id_order))) { + if($objOrder->id_channel_manager_booking == NULL && ($objOrder->source == Configuration::get('PS_SHOP_DOMAIN'))) return; + } + + $objAriUpdates = new QcmcAriUpdates(); + + $idBooking = (int)$objHotelBookingDetail->id; + $oldBooking = new HotelBookingDetail($idBooking); + + $oldRoomTypeId = (int)$oldBooking->id_product; + $idHotel = (int)$oldBooking->id_hotel; + $oldDateFrom = date('Y-m-d', strtotime($oldBooking->date_from)); + $oldDateTo = date('Y-m-d', strtotime($oldBooking->date_to)); + + $newRoomTypeId = (int)$objHotelBookingDetail->id_product; + $newDateFrom = date('Y-m-d', strtotime($objHotelBookingDetail->date_from)); + $newDateTo = date('Y-m-d', strtotime($objHotelBookingDetail->date_to)); + + // is_cancelled/is_refunded excluded: not in the ObjectModel definition, and only + // ever changed via direct SQL (handlePutRequest), which this hook can't observe. + $fieldsToCheck = [ + 'check_in', + 'check_out', + 'id_status', + ]; + + foreach ($fieldsToCheck as $field) { + if ($oldBooking->$field != $objHotelBookingDetail->$field) { + $objAriUpdates->saveAriUpdateRow( + $idHotel, + $oldRoomTypeId, + $oldDateFrom, + $oldDateTo + ); + break; + } + } + + if (($oldRoomTypeId == $newRoomTypeId) && (($oldDateFrom != $newDateFrom) || ($oldDateTo != $newDateTo))) { + $objAriUpdates->saveAriUpdateRow( + $idHotel, + $oldRoomTypeId, + $oldDateFrom, + $oldDateTo + ); + + $objAriUpdates->saveAriUpdateRow( + $idHotel, + $newRoomTypeId, + $newDateFrom, + $newDateTo + ); + } + } + } + + //Handles Ari update when a new order is added, it also handles the cond. where a room is added to an existing order + public function hookActionObjectHotelBookingDetailAddAfter($params) + { + if (isset($params['object']) && ($params['object'] instanceof HotelBookingDetail)) { + $objAriUpdates = new QcmcAriUpdates(); + $objHotelBookingDetail = $params['object']; + + if (Validate::isLoadedObject($objOrder = new Order((int)$objHotelBookingDetail->id_order))) { + if($objOrder->id_channel_manager_booking == NULL && ($objOrder->source == Configuration::get('PS_SHOP_DOMAIN'))) return; + } + + $objAriUpdates->saveAriUpdateRow( + (int)$objHotelBookingDetail->id_hotel, + (int)$objHotelBookingDetail->id_product, + date('Y-m-d', strtotime($objHotelBookingDetail->date_from)), + date('Y-m-d', strtotime($objHotelBookingDetail->date_to)) + ); + } + } + + //Handles Ari update when an order is deleted, it also handles the cond. where a room is deleted from an existing order + public function hookActionObjectHotelBookingDetailDeleteAfter($params) + { + if (isset($params['object']) && ($params['object'] instanceof HotelBookingDetail)) { + $objAriUpdates = new QcmcAriUpdates(); + $objHotelBookingDetail = $params['object']; + + if (Validate::isLoadedObject($objOrder = new Order((int)$objHotelBookingDetail->id_order))) { + if($objOrder->id_channel_manager_booking == NULL && ($objOrder->source == Configuration::get('PS_SHOP_DOMAIN'))) return; + } + + $objAriUpdates->saveAriUpdateRow( + (int)$objHotelBookingDetail->id_hotel, + (int)$objHotelBookingDetail->id_product, + date('Y-m-d', strtotime($objHotelBookingDetail->date_from)), + date('Y-m-d', strtotime($objHotelBookingDetail->date_to)) + ); + } + } + + public function hookActionObjectOrderPropertiesModifier(&$params) + { + if(is_array($params['obj_properties']) && !isset($params['obj_properties']['id_channel_manager_booking'])){ + $params['obj_properties']['id_channel_manager_booking'] = null; + } + } + + public function hookActionObjectOrderDefinitionModifier(&$params) + { + if(is_array($params['definition']['fields']) && !isset($params['definition']['fields']['id_channel_manager_booking'])){ + $params['definition']['fields']['id_channel_manager_booking'] = array('type' => Order::TYPE_STRING); + } + } + + public function hookActionObjectHotelBookingDetailPropertiesModifier(&$params) + { + if (is_array($params['obj_properties']) && !isset($params['obj_properties']['rate_plan_name'])) { + $params['obj_properties']['rate_plan_name'] = null; + } + } + + public function hookActionObjectHotelBookingDetailDefinitionModifier(&$params) + { + if (is_array($params['definition']['fields']) && !isset($params['definition']['fields']['rate_plan_name'])) { + $params['definition']['fields']['rate_plan_name'] = array( + 'type' => HotelBookingDetail::TYPE_STRING, + 'default' => null + ); + } + } + + public function hookActionObjectOrderAddBefore(&$params) + { + if (!isset($params['object']->id_channel_manager_booking)) { + $params['object']->id_channel_manager_booking = NULL; + } + } + + public function hookActionObjectOrderUpdateAfter(&$params) + { + if (!isset($params['object']->id_channel_manager_booking)) { + $params['object']->id_channel_manager_booking = NULL; + } + } + + //Handles Ari update when a product (room type) is updated + //Tracks: price, tax_rate, active, show_at_front for room types + //Tracks: auto_add_to_cart for service products linked to room types + public function hookActionObjectProductUpdateBefore($params) + { + if (isset($params['object']) && ($params['object'] instanceof Product)) { + $objProduct = $params['object']; + + // Check if this product is a hotel room type + $objHotelRoomType = new HotelRoomType(); + $roomTypeInfo = $objHotelRoomType->getRoomTypeInfoByIdProduct($objProduct->id); + + $hasChanges = false; + $ariData = []; + + if ($roomTypeInfo) { + // This is a room type product - check standard fields + $idHotel = (int)$roomTypeInfo['id_hotel']; + $idRoomType = (int)$objProduct->id; + + // Get the old product data for comparison + $oldProduct = new Product($objProduct->id); + + // Fields to check for room type changes + $fieldsToCheck = [ + 'price', + 'id_tax_rules_group', + 'active', + 'show_at_front' + ]; + + foreach ($fieldsToCheck as $field) { + if (isset($objProduct->$field) && isset($oldProduct->$field)) { + if ($objProduct->$field != $oldProduct->$field) { + $hasChanges = true; + + // Handle active/show_at_front changes - set inventory + if ($field == 'active' || $field == 'show_at_front') { + $ariData['inventory'] = 1; + } + + // Handle price/tax changes - set price + if ($field == 'price' || $field == 'id_tax_rules_group') { + $ariData['price'] = 1; + } + } + } + } + + // If changes detected, save ARI update + if ($hasChanges) { + // Get booking offset for the hotel + $maxBookingOffset = HotelOrderRestrictDate::getMaxOrderDate($idHotel); + + // Save ARI update + $objAriUpdates = new QcmcAriUpdates(); + $objAriUpdates->saveAriUpdateRow( + $idHotel, + $idRoomType, + date('Y-m-d'), + $maxBookingOffset, + $ariData + ); + } + } else { + // This product might be a service product - check if it's linked to a room type + // Use core method getAssociatedHotelsAndRoomType + $objRoomTypeServiceProduct = new RoomTypeServiceProduct(); + $serviceAssociations = $objRoomTypeServiceProduct->getAssociatedHotelsAndRoomType( + $objProduct->id, + RoomTypeServiceProduct::WK_ELEMENT_TYPE_ROOM_TYPE + ); + + if (!empty($serviceAssociations['room_type'])) { + // This is a service product linked to room types + // Check if auto_add_to_cart changed + $oldProduct = new Product($objProduct->id); + + if (isset($objProduct->auto_add_to_cart) && isset($oldProduct->auto_add_to_cart)) { + if ($objProduct->auto_add_to_cart != $oldProduct->auto_add_to_cart) { + // auto_add_to_cart changed - trigger price update for all linked room types + $objAriUpdates = new QcmcAriUpdates(); + + foreach ($serviceAssociations['room_type'] as $idRoomType) { + // Get hotel info for this room type + $roomTypeInfo = $objHotelRoomType->getRoomTypeInfoByIdProduct($idRoomType); + + if ($roomTypeInfo) { + $idHotel = (int)$roomTypeInfo['id_hotel']; + $maxBookingOffset = HotelOrderRestrictDate::getMaxOrderDate($idHotel); + + // Save ARI update with price key + $objAriUpdates->saveAriUpdateRow( + $idHotel, + (int)$idRoomType, + date('Y-m-d'), + $maxBookingOffset, + array('price' => 1) + ); + } + } + } + } + } + } + } + } + + // Skip rate ARI when id_cart > 0 (per-booking price) or CM API request active. + // Admin pricing rules always have id_cart = 0 and must queue rate updates. + private function shouldSkipFeaturePriceAriUpdate($objFeaturePrice) + { + if ((int)$objFeaturePrice->id_cart > 0) { + return true; + } + if (self::$idBookingCm !== null) { + return true; + } + return false; + } + + //Handles Ari update when feature prices (advance price) are added + public function hookActionObjectHotelRoomTypeFeaturePricingAddBefore($params) + { + if (!isset($params['object']) || !($params['object'] instanceof HotelRoomTypeFeaturePricing)) { + return; + } + $objFeaturePrice = $params['object']; + if ($this->shouldSkipFeaturePriceAriUpdate($objFeaturePrice)) { + return; + } + + $objHotelRoomType = new HotelRoomType(); + $roomTypeInfo = $objHotelRoomType->getRoomTypeInfoByIdProduct($objFeaturePrice->id_product); + if (!$roomTypeInfo) { + return; + } + + $idHotel = (int)$roomTypeInfo['id_hotel']; + $idRoomType = (int)$objFeaturePrice->id_product; + $maxBookingOffset = HotelOrderRestrictDate::getMaxOrderDate($idHotel); + + $objAriUpdates = new QcmcAriUpdates(); + $objAriUpdates->saveAriUpdateRow( + $idHotel, + $idRoomType, + date('Y-m-d'), + $maxBookingOffset, + array('rate' => 1) + ); + } + + //Handles Ari update when feature prices (advance price) settings are updated + public function hookActionObjectHotelRoomTypeFeaturePricingUpdateBefore($params) + { + if (!isset($params['object']) || !($params['object'] instanceof HotelRoomTypeFeaturePricing)) { + return; + } + $objFeaturePrice = $params['object']; + if ($this->shouldSkipFeaturePriceAriUpdate($objFeaturePrice)) { + return; + } + + $objHotelRoomType = new HotelRoomType(); + $roomTypeInfo = $objHotelRoomType->getRoomTypeInfoByIdProduct($objFeaturePrice->id_product); + if (!$roomTypeInfo) { + return; + } + + $idHotel = (int)$roomTypeInfo['id_hotel']; + $idRoomType = (int)$objFeaturePrice->id_product; + $maxBookingOffset = HotelOrderRestrictDate::getMaxOrderDate($idHotel); + + $objAriUpdates = new QcmcAriUpdates(); + $objAriUpdates->saveAriUpdateRow( + $idHotel, + $idRoomType, + date('Y-m-d'), + $maxBookingOffset, + array('rate' => 1) + ); + } + + //Handles Ari update when feature prices (advance price) are deleted + public function hookActionObjectHotelRoomTypeFeaturePricingDeleteBefore($params) + { + if (!isset($params['object']) || !($params['object'] instanceof HotelRoomTypeFeaturePricing)) { + return; + } + $objFeaturePrice = $params['object']; + if ($this->shouldSkipFeaturePriceAriUpdate($objFeaturePrice)) { + return; + } + + $objHotelRoomType = new HotelRoomType(); + $roomTypeInfo = $objHotelRoomType->getRoomTypeInfoByIdProduct($objFeaturePrice->id_product); + if (!$roomTypeInfo) { + return; + } + + $idHotel = (int)$roomTypeInfo['id_hotel']; + $idRoomType = (int)$objFeaturePrice->id_product; + + // Capture restriction date ranges before delete() removes them + $objFeaturePriceRestriction = new HotelRoomTypeFeaturePricingRestriction(); + $restrictions = $objFeaturePriceRestriction->getRestrictionsByIdFeaturePrice($objFeaturePrice->id); + + $objAriUpdates = new QcmcAriUpdates(); + $hasRestrictions = false; + + if ($restrictions) { + foreach ($restrictions as $restriction) { + if (!empty($restriction['date_from']) && !empty($restriction['date_to'])) { + $hasRestrictions = true; + $objAriUpdates->saveAriUpdateRow( + $idHotel, + $idRoomType, + date('Y-m-d', strtotime($restriction['date_from'])), + date('Y-m-d', strtotime($restriction['date_to'])), + array('rate' => 1) + ); + } + } + } + + if (!$hasRestrictions) { + $maxBookingOffset = HotelOrderRestrictDate::getMaxOrderDate($idHotel); + $objAriUpdates->saveAriUpdateRow( + $idHotel, + $idRoomType, + date('Y-m-d'), + $maxBookingOffset, + array('rate' => 1) + ); + } + } + + //Handles Ari update when a service product is linked to a room type + public function hookActionObjectRoomTypeServiceProductLinkBefore($params) + { + if (!isset($params['id_product']) || !isset($params['values']) || !isset($params['element_type'])) { + return; + } + + $idServiceProduct = (int)$params['id_product']; + $values = $params['values']; + $elementType = (int)$params['element_type']; + + // Only process room type associations + if ($elementType != RoomTypeServiceProduct::WK_ELEMENT_TYPE_ROOM_TYPE) { + return; + } + + // Check if this service product is auto-added + $objProduct = new Product($idServiceProduct); + if (!$objProduct->auto_add_to_cart) { + return; + } + + $objHotelRoomType = new HotelRoomType(); + $objAriUpdates = new QcmcAriUpdates(); + + foreach ($values as $idRoomType) { + $roomTypeInfo = $objHotelRoomType->getRoomTypeInfoByIdProduct((int)$idRoomType); + if ($roomTypeInfo) { + $idHotel = (int)$roomTypeInfo['id_hotel']; + $maxBookingOffset = HotelOrderRestrictDate::getMaxOrderDate($idHotel); + + $objAriUpdates->saveAriUpdateRow( + $idHotel, + (int)$idRoomType, + date('Y-m-d'), + $maxBookingOffset, + array('price' => 1) + ); + } + } + } + + //Handles Ari update when a service product is unlinked from a room type + public function hookActionObjectRoomTypeServiceProductUnlinkBefore($params) + { + if (!isset($params['id_product']) || !isset($params['elements'])) { + return; + } + + $idServiceProduct = (int)$params['id_product']; + $elements = $params['elements']; + + if (empty($elements)) { + return; + } + + // Check if this service product is auto-added + $objProduct = new Product($idServiceProduct); + if (!$objProduct->auto_add_to_cart) { + return; + } + + $objHotelRoomType = new HotelRoomType(); + $objAriUpdates = new QcmcAriUpdates(); + + foreach ($elements as $element) { + if ((int)$element['element_type'] == RoomTypeServiceProduct::WK_ELEMENT_TYPE_ROOM_TYPE) { + $roomTypeInfo = $objHotelRoomType->getRoomTypeInfoByIdProduct((int)$element['id_element']); + if ($roomTypeInfo) { + $idHotel = (int)$roomTypeInfo['id_hotel']; + $maxBookingOffset = HotelOrderRestrictDate::getMaxOrderDate($idHotel); + + $objAriUpdates->saveAriUpdateRow( + $idHotel, + (int)$element['id_element'], + date('Y-m-d'), + $maxBookingOffset, + array('price' => 1) + ); + } + } + } + } + + //Handles Ari update when hotel room type is updated (LOS restriction changes at room type level) + public function hookActionObjectHotelRoomTypeUpdateBefore($params) + { + if (isset($params['object']) && ($params['object'] instanceof HotelRoomType)) { + $objRoomType = $params['object']; + + // Get hotel info for this room type + $objHotelRoomType = new HotelRoomType(); + $roomTypeInfo = $objHotelRoomType->getRoomTypeInfoByIdProduct($objRoomType->id); + if (!$roomTypeInfo) { + return; + } + + $idHotel = (int)$roomTypeInfo['id_hotel']; + $idRoomType = (int)$objRoomType->id; + + // Get the old room type data for comparison + $oldRoomType = new HotelRoomType($objRoomType->id); + + // Check if min_los or max_los changed + $losChanged = false; + if ($objRoomType->min_los != $oldRoomType->min_los + || $objRoomType->max_los != $oldRoomType->max_los) { + $losChanged = true; + } + + if ($losChanged) { + // Get booking offset for the hotel + $maxBookingOffset = HotelOrderRestrictDate::getMaxOrderDate($idHotel); + + // Save ARI update with los key + $objAriUpdates = new QcmcAriUpdates(); + $objAriUpdates->saveAriUpdateRow( + $idHotel, + $idRoomType, + date('Y-m-d'), + $maxBookingOffset, + ['los' => 1] + ); + } + } + } + + + //Handles Ari update when room type restriction date range is updated (LOS changes) + public function hookActionObjectHotelRoomTypeRestrictionDateRangeUpdateBefore($params) + { + $this->manageLosRestriction($params); + } + + //Handles Ari update when room type restriction date range is added (LOS added) + public function hookActionObjectHotelRoomTypeRestrictionDateRangeAddBefore($params) + { + $this->manageLosRestriction($params); + } + + //Handles Ari update when room type restriction date range is deleted (LOS removed) + public function hookActionObjectHotelRoomTypeRestrictionDateRangeDeleteBefore($params) + { + if (isset($params['object']) && ($params['object'] instanceof HotelRoomTypeRestrictionDateRange)) { + $objRestriction = $params['object']; + + // Get room type info + $objHotelRoomType = new HotelRoomType(); + $roomTypeInfo = $objHotelRoomType->getRoomTypeInfoByIdProduct($objRestriction->id_product); + + if (!$roomTypeInfo) { + return; + } + + $idHotel = (int)$roomTypeInfo['id_hotel']; + $idRoomType = (int)$objRestriction->id_product; + + // Save ARI update with los key for the deleted restriction date range + $objAriUpdates = new QcmcAriUpdates(); + $objAriUpdates->saveAriUpdateRow( + $idHotel, + $idRoomType, + date('Y-m-d', strtotime($objRestriction->date_from)), + date('Y-m-d', strtotime($objRestriction->date_to)), + ['los' => 1] + ); + } + } + + public function hookActionAvailRoomSearchSqlModifier(&$params) + { + // CM only: trusted IP, or a cm_api booking request in progress. + if (!QcmcChannelManagerApiService::isAllowedRemoteAddr($_SERVER['REMOTE_ADDR']) + && self::$idBookingCm === null + ) { + return; + } + + // Check if id_order is passed in params + if (isset($params['params']['all_params']['id_order']) && $params['params']['all_params']['id_order']) { + $idOrder = (int) $params['params']['all_params']['id_order']; + + // Modify the checked_out subquery to exclude rooms based on order ID + if (isset($params['exclude_room_id']['checked_out'])) { + // The modified order's own rooms must not block its search (anchored on the leading WHERE). + $params['exclude_room_id']['checked_out'] = preg_replace( + '/WHERE\s+`id_hotel`\s*=/', + 'WHERE `id_order` != ' . $idOrder . ' AND `id_hotel` =', + $params['exclude_room_id']['checked_out'], + 1 + ); + } + } + + // [LOS BYPASS] Bypass LOS (Length of Stay) stay validation when requested by channel manager + // This allows the channel manager to skip min_los/max_los restrictions during room search + if (isset($params['exclude_room_id']['length_of_stay'])) { + unset($params['exclude_room_id']['length_of_stay']); + } + // [END LOS BYPASS] + } + + public function manageLosRestriction($params) + { + if (isset($params['object']) && ($params['object'] instanceof HotelRoomTypeRestrictionDateRange)) { + $objRestriction = $params['object']; + + // Get room type info + $objHotelRoomType = new HotelRoomType(); + $roomTypeInfo = $objHotelRoomType->getRoomTypeInfoByIdProduct($objRestriction->id_product); + + if (!$roomTypeInfo) { + return; + } + + $idHotel = (int)$roomTypeInfo['id_hotel']; + $idRoomType = (int)$objRestriction->id_product; + + // Get the old restriction data for comparison + $oldRestriction = new HotelRoomTypeRestrictionDateRange($objRestriction->id); + + // Check if min_los or max_los changed + $losChanged = false; + if ($objRestriction->min_los != $oldRestriction->min_los + || $objRestriction->max_los != $oldRestriction->max_los) { + $losChanged = true; + } + + if ($losChanged) { + // Get booking offset for the hotel + $maxBookingOffset = HotelOrderRestrictDate::getMaxOrderDate($idHotel); + + // Save ARI update with rate = 1 for LOS changes + $objAriUpdates = new QcmcAriUpdates(); + $objAriUpdates->saveAriUpdateRow( + $idHotel, + $idRoomType, + date('Y-m-d', strtotime($objRestriction->date_from)), + date('Y-m-d', strtotime($objRestriction->date_to)), + ['los' => 1] + ); + } + } + } + + //Handles Ari update when hotel order restrict date is updated (booking offset changes) + public function hookActionObjectHotelOrderRestrictDateUpdateBefore($params) + { + if (isset($params['object']) && ($params['object'] instanceof HotelOrderRestrictDate)) { + $objOrderRestrict = $params['object']; + + // Get all room types for this hotel + $objHotelRoomType = new HotelRoomType(); + $roomTypes = $objHotelRoomType->getRoomTypeByHotelId($objOrderRestrict->id_hotel, $this->context->language->id); + + if (!$roomTypes) { + return; + } + + // Get the old order restrict data for comparison + $oldOrderRestrict = new HotelOrderRestrictDate($objOrderRestrict->id); + + // Check if booking offset fields changed + $offsetChanged = false; + if ($objOrderRestrict->max_checkout_offset != $oldOrderRestrict->max_checkout_offset + || $objOrderRestrict->min_booking_offset != $oldOrderRestrict->min_booking_offset + || $objOrderRestrict->use_global_max_checkout_offset != $oldOrderRestrict->use_global_max_checkout_offset + || $objOrderRestrict->use_global_min_booking_offset != $oldOrderRestrict->use_global_min_booking_offset) { + $offsetChanged = true; + } + + if ($offsetChanged) { + // Get new max booking offset date using booking offset + $bookingOffsetDate = HotelOrderRestrictDate::getMaxOrderDate($oldOrderRestrict->id_hotel); + + // Get max checkout offset date using checkout offset + $checkoutOffsetDate = HotelOrderRestrictDate::getMaximumCheckoutOffset($oldOrderRestrict->id_hotel); + + // Use the maximum of both as end date + $maxBookingOffsetDays = max($bookingOffsetDate, $checkoutOffsetDate); + + // Save ARI update for all room types in this hotel + $objAriUpdates = new QcmcAriUpdates(); + foreach ($roomTypes as $roomType) { + $objAriUpdates->saveAriUpdateRow( + (int)$objOrderRestrict->id_hotel, + (int)$roomType['id_product'], + date('Y-m-d'), + $bookingOffsetDate, + ['min_booking_offset' => HotelOrderRestrictDate::getMinimumBookingOffset($oldOrderRestrict->id_hotel), 'max_booking_offset' => $checkoutOffsetDate] + ); + } + } + } + } + + + public function hookActionPartiallyAvailRoomSearchSqlModifier(&$params) + { + // CM only: trusted IP, or a cm_api booking request in progress. + if (!QcmcChannelManagerApiService::isAllowedRemoteAddr($_SERVER['REMOTE_ADDR']) + && self::$idBookingCm === null + ) { + return; + } + + // Check if id_order is passed in params + if (isset($params['params']['all_params']['id_order']) && $params['params']['all_params']['id_order']) { + $idOrder = (int) $params['params']['all_params']['id_order']; + + // Modify the SQL queries to exclude rooms based on order ID + if (isset($params['sql']) && is_array($params['sql'])) { + foreach ($params['sql'] as &$sqlQuery) { + // Booking detail query only: the modified order's own rooms must not block its search. + if (strpos($sqlQuery, '`'._DB_PREFIX_.'htl_booking_detail` AS bd') !== false) { + $sqlQuery = preg_replace( + '/WHERE\s+bd\.`id_hotel`\s*=/', + 'WHERE bd.`id_order` != ' . $idOrder . ' AND bd.`id_hotel`=', + $sqlQuery, + 1 + ); + } + } + unset($sqlQuery); + } + } + } + + public function addConfigurationValues() + { + $configKeys = [ + 'QCMC_CM_CLIENT_ID', + 'QCMC_CM_CLIENT_SECRET', + ]; + + foreach ($configKeys as $key) { + if (!Configuration::hasKey($key)) { + $success = Configuration::updateValue($key, '', false, null, null); + if (!$success) { + return false; + } + } + } + + return true; + } + + public function install() + { + $objModuleDb = new QcmcCMConnectorDb(); + if ( !parent::install() + || !$this->registerModuleHooks() + || !$objModuleDb->createTables() + || !$this->addConfigurationValues() + || !$this->removeOldChannelManagerConnectorModule() + || !$this->callInstallTab() + ) { + return false; + } + return true; + } + + public function hookDisplayOrderRoomsBookingsTableHeading($params) + { + $objOrder = isset($params['order']) ? $params['order'] : null; + if (!$objOrder || !Validate::isLoadedObject($objOrder)) { + return ''; + } else if (empty($objOrder->id_channel_manager_booking)) { + return ''; + } else { + return '' . $this->l('Rate Plan') . ''; + } + } + + public function hookDisplayOrderRoomsBookingsTableData($params) + { + $objOrder = isset($params['order']) ? $params['order'] : null; + if (!$objOrder || !Validate::isLoadedObject($objOrder)) { + return ''; + } else if (empty($objOrder->id_channel_manager_booking)) { + return ''; + } else { + $ratePlanName = isset($params['data']['rate_plan_name']) ? $params['data']['rate_plan_name'] : '--'; + return '' . htmlspecialchars($ratePlanName) . ''; + } + } + + public function hookDisplayBackOfficeHeader() + { + // Loaded on every BO page (not gated on controller_name) — the tab's menu icon + // (.icon-AdminQloappsChannelManagerConnector) renders in the persistent sidebar + // menu across all admin pages, not just this controller's own page. + $this->context->controller->addCSS($this->_path.'views/css/admin/admin_tab_logo.css'); + } + + public function hookDisplayAdminListBefore() + { + // This tpl will only display when connected with channel manager (CM booking exists or API credentials saved) + if (self::CM_BOOKING_LIST_TAB_CLASS == $this->context->controller->controller_name + && QcmcChannelManagerOrder::isChannelManagerConnected() + ) { + return $this->display(__FILE__, 'qcmc_channel_manager_connection_info.tpl'); + } + } + + public function hookActionAdminControllerSetMedia() + { + // This CSS will only apply when connected with channel manager (CM booking exists or API credentials saved) + if (self::CM_BOOKING_LIST_TAB_CLASS == $this->context->controller->controller_name + && QcmcChannelManagerOrder::isChannelManagerConnected() + ) { + $this->context->controller->addCSS(_MODULE_DIR_.$this->name.'/views/css/hook/qcmc_cm_connection_info.css'); + } + } + + public function callInstallTab() + { + return $this->installTab(self::CM_BOOKING_LIST_TAB_CLASS, 'Channel Manager'); + } + + public function installTab($class_name, $tab_name, $tab_parent_name = false, $need_tab = true) + { + // Defensive: remove any existing Tab row with this class_name first (leftover from the + // old qlochannelmanagerconnector module, or a previous partial install/upgrade run) so we + // never end up with two rows sharing the same class_name. + if ($existingIdTab = (int) Tab::getIdFromClassName($class_name)) { + $existingTab = new Tab($existingIdTab); + if (Validate::isLoadedObject($existingTab)) { + $existingTab->delete(); + } + } + + $tab = new Tab(); + $tab->active = 1; + $tab->class_name = $class_name; + $tab->name = array(); + + foreach (Language::getLanguages(true) as $lang) { + $tab->name[$lang['id_lang']] = $tab_name; + } + + if ($tab_parent_name) { + $tab->id_parent = (int) Tab::getIdFromClassName($tab_parent_name); + } elseif (!$need_tab) { + $tab->id_parent = -1; + } else { + $tab->id_parent = 0; + } + + $tab->module = $this->name; + $res = $tab->add(); + + //Set position Tab + $objTab = new Tab($tab->id); + + // position of the tab will be after hotelreservationsystem module tab + // without calling this function we are getting hotelreservationsystem module tab cache object with old position + ObjectModel::disableCache(); + if (Validate::isLoadedObject( + $objTabForPosition = Tab::getInstanceFromClassName('AdminHotelReservationSystemManagement') + )) { + $objTab->updatePosition(0, ($objTabForPosition->position + 1)); + } else { + $objTab->updatePosition(0, 6); + } + // enable cache variable again + ObjectModel::enableCache(); + + return $res; + } + + public function uninstallTab() + { + $moduleTabs = Tab::getCollectionFromModule($this->name); + if (!empty($moduleTabs)) { + foreach ($moduleTabs as $moduleTab) { + $moduleTab->delete(); + } + } + + // Defensive: also purge any orphaned Tab row with our class_name not attributed to this module. + if ($idTab = (int) Tab::getIdFromClassName(self::CM_BOOKING_LIST_TAB_CLASS)) { + $orphanTab = new Tab($idTab); + if (Validate::isLoadedObject($orphanTab)) { + $orphanTab->delete(); + } + } + + return true; + } + + public function removeOldChannelManagerConnectorModule() + { + $oldModuleName = 'qlochannelmanagerconnector'; + + // Only attempt uninstall if the module is actually installed (Module::isInstalled() + // checks the ps_module row directly) — don't try to load/instantiate it otherwise. + if (Module::isInstalled($oldModuleName)) { + $oldModule = Module::getInstanceByName($oldModuleName); + // uninstall() already runs the old module's own dropTables() + uninstallTab() — + // table and tab cleanup is handled by that call, nothing extra needed here. + if (Validate::isLoadedObject($oldModule)) { + $oldModule->uninstall(); + } + } + + $oldModuleDir = _PS_MODULE_DIR_.$oldModuleName; + if (is_dir($oldModuleDir)) { + Tools::deleteDirectory($oldModuleDir); + } + + return true; + } + + public function registerModuleHooks() + { + return $this->registerHook( + array( + 'addWebserviceResources', + 'actionRoomReallocateAfter', + 'actionObjectHotelRoomDisableDatesAddAfter', + 'actionObjectHotelRoomDisableDatesDeleteAfter', + 'actionValidateRoomInformation', + 'actionObjectHotelRoomInformationAddAfter', + 'actionObjectHotelRoomInformationDeleteAfter', + 'actionObjectHotelBookingDetailAddAfter', + 'actionObjectHotelBookingDetailUpdateBefore', + 'actionObjectHotelBookingDetailDeleteAfter', + 'actionObjectOrderAddBefore', + 'actionObjectOrderUpdateAfter', + 'actionAvailRoomSearchSqlModifier', + 'actionPartiallyAvailRoomSearchSqlModifier', + // added hook to manage the rate plan name + 'actionObjectHotelBookingDetailPropertiesModifier', + 'actionObjectHotelBookingDetailDefinitionModifier', + 'displayOrderRoomsBookingsTableHeading', + 'displayOrderRoomsBookingsTableData', + // added hook to add id_channel_manager_booking to the order object + 'actionObjectOrderPropertiesModifier', + 'actionObjectOrderDefinitionModifier', + // added hooks to check room type status, price and restriction related changes + 'actionObjectProductUpdateBefore', + 'actionObjectHotelRoomTypeUpdateBefore', + 'actionObjectHotelRoomTypeRestrictionDateRangeUpdateBefore', + 'actionObjectHotelRoomTypeRestrictionDateRangeAddBefore', + 'actionObjectHotelOrderRestrictDateUpdateBefore', + 'actionObjectHotelRoomTypeRestrictionDateRangeDeleteBefore', + // hooks for Advanced Price Rules (Feature Pricing) ARI updates + 'actionObjectHotelRoomTypeFeaturePricingAddBefore', + 'actionObjectHotelRoomTypeFeaturePricingUpdateBefore', + 'actionObjectHotelRoomTypeFeaturePricingDeleteBefore', + // hooks for auto-added service product ARI updates + 'actionObjectRoomTypeServiceProductLinkBefore', + 'actionObjectRoomTypeServiceProductUnlinkBefore', + // hooks for the absorbed channel-manager booking list admin feature + 'displayAdminListBefore', + 'displayBackOfficeHeader', + 'actionAdminControllerSetMedia', + ) + ); + } + + public function deleteConfigurationValues() + { + $configKeys = [ + 'QCMC_CM_CLIENT_ID', + 'QCMC_CM_CLIENT_SECRET', + 'QCMC_CM_ACCESS_TOKEN', + ]; + + foreach ($configKeys as $key) { + Configuration::deleteByName($key); + } + + return true; + } + + public function uninstall() + { + $objModuleDb = new QcmcCMConnectorDb(); + if ( !parent::uninstall() + || !$objModuleDb->dropTables() + || !$this->deleteConfigurationValues() + || !$this->uninstallTab() + ) { + return false; + } + return true; + } +} diff --git a/modules/qlochannelmanagerconnector/views/css/index.php b/modules/qlocmconnector/upgrade/index.php similarity index 100% rename from modules/qlochannelmanagerconnector/views/css/index.php rename to modules/qlocmconnector/upgrade/index.php diff --git a/modules/qlocmconnector/upgrade/upgrade-5.0.0.php b/modules/qlocmconnector/upgrade/upgrade-5.0.0.php new file mode 100644 index 000000000..3bf13b9ab --- /dev/null +++ b/modules/qlocmconnector/upgrade/upgrade-5.0.0.php @@ -0,0 +1,110 @@ +createTables() + && $objCMConnector->addConfigKeys() + && $objCMConnector->registerHooks() + ); +} +class UpgradeQloCMConnector500 +{ + public function __construct($module) + { + $this->module = $module; + } + + public function createTables() + { + $sql = array( + "CREATE TABLE IF NOT EXISTS "._DB_PREFIX_."ari_updates ( + `id_ari_updates` INT AUTO_INCREMENT, + `id_hotel` INT NOT NULL, + `id_room_type` INT NOT NULL, + `date` DATETIME NOT NULL, + `ari_data` LONGTEXT NULL, + `status` TINYINT(1) NOT NULL DEFAULT 0, + `date_add` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + `date_upd` DATETIME NULL, + PRIMARY KEY (`id_ari_updates`) + )ENGINE=" . _MYSQL_ENGINE_ ." DEFAULT CHARSET=utf8", + "ALTER TABLE `"._DB_PREFIX_."orders` + ADD COLUMN `id_channel_manager_booking` VARCHAR(255) NULL", + ); + + $objDb = Db::getInstance(); + foreach ($sql as $query) { + if (!$objDb->execute(trim($query))) { + return false; + } + } + return true; + } + + public function addConfigKeys() + { + $configKeys = [ + 'QCMC_CM_CLIENT_ID', + 'QCMC_CM_CLIENT_SECRET', + ]; + + foreach ($configKeys as $key) { + if (!Configuration::hasKey($key)) { + $success = Configuration::updateValue($key, '', false, null, null); + if (!$success) { + return false; + } + } + } + + return true; + } + + public function registerHooks() + { + return $this->module->registerHook( + array( + 'actionRoomReallocateAfter', + 'actionObjectHotelRoomDisableDatesAddAfter', + 'actionObjectHotelRoomDisableDatesDeleteAfter', + 'actionValidateRoomInformation', + 'actionObjectHotelRoomInformationAddAfter', + 'actionObjectHotelRoomInformationDeleteAfter', + 'actionObjectHotelBookingDetailAddAfter', + 'actionObjectHotelBookingDetailUpdateBefore', + 'actionObjectHotelBookingDetailDeleteAfter', + 'actionObjectOrderPropertiesModifier', + 'actionObjectOrderDefinitionModifier', + 'actionObjectOrderAddBefore', + 'actionObjectOrderUpdateAfter' + ) + ); + } +} diff --git a/modules/qlocmconnector/upgrade/upgrade-5.0.2.php b/modules/qlocmconnector/upgrade/upgrade-5.0.2.php new file mode 100644 index 000000000..6ba19e652 --- /dev/null +++ b/modules/qlocmconnector/upgrade/upgrade-5.0.2.php @@ -0,0 +1,50 @@ +registerHooks(); +} + +class UpgradeQloCMConnector502 +{ + public function __construct($module) + { + $this->module = $module; + } + + public function registerHooks() + { + return $this->module->registerHook( + array( + 'actionAvailRoomSearchSqlModifier', + 'actionPartiallyAvailRoomSearchSqlModifier', + ) + ); + } +} diff --git a/modules/qlocmconnector/upgrade/upgrade-5.0.3.php b/modules/qlocmconnector/upgrade/upgrade-5.0.3.php new file mode 100644 index 000000000..bf2f0a244 --- /dev/null +++ b/modules/qlocmconnector/upgrade/upgrade-5.0.3.php @@ -0,0 +1,60 @@ +registerHooks() + ); +} + +class UpgradeQloCMConnector503{ + public function __construct($module) + { + $this->module = $module; + } + + public function registerHooks() + { + return $this->module->registerHook( + array( + 'actionObjectProductUpdateBefore', + 'actionObjectHotelRoomTypeUpdateBefore', + 'actionObjectHotelRoomTypeRestrictionDateRangeUpdateBefore', + 'actionObjectHotelRoomTypeRestrictionDateRangeAddBefore', + 'actionObjectHotelOrderRestrictDateUpdateBefore', + 'actionObjectHotelRoomTypeRestrictionDateRangeDeleteBefore', + 'actionObjectHotelRoomTypeFeaturePricingAddBefore', + 'actionObjectHotelRoomTypeFeaturePricingUpdateBefore', + 'actionObjectHotelRoomTypeFeaturePricingDeleteBefore', + 'actionObjectRoomTypeServiceProductLinkBefore', + 'actionObjectRoomTypeServiceProductUnlinkBefore' + ) + ); + } +} diff --git a/modules/qlocmconnector/upgrade/upgrade-5.0.4.php b/modules/qlocmconnector/upgrade/upgrade-5.0.4.php new file mode 100644 index 000000000..5b4fe8f6f --- /dev/null +++ b/modules/qlocmconnector/upgrade/upgrade-5.0.4.php @@ -0,0 +1,71 @@ +registerHooks() + && $objCMConnector->updateTable() + ); +} + +class UpgradeQloCMConnector504{ + public function __construct($module) + { + $this->module = $module; + } + + public function registerHooks() + { + return $this->module->registerHook( + array( + 'displayOrderRoomsBookingsTableHeading', + 'displayOrderRoomsBookingsTableData', + 'actionObjectHotelBookingDetailPropertiesModifier', + 'actionObjectHotelBookingDetailDefinitionModifier', + ) + ); + } + + public function updateTable() + { + $exists = Db::getInstance()->getValue( + "SELECT COLUMN_NAME FROM INFORMATION_SCHEMA.COLUMNS + WHERE TABLE_SCHEMA = DATABASE() + AND TABLE_NAME = '" . pSQL(_DB_PREFIX_ . 'htl_booking_detail') . "' + AND COLUMN_NAME = 'rate_plan_name'" + ); + if (!$exists) { + $sql = "ALTER TABLE `" . _DB_PREFIX_ . "htl_booking_detail` ADD COLUMN `rate_plan_name` VARCHAR(255) DEFAULT NULL"; + if (!Db::getInstance()->execute($sql)) { + return false; + } + } + return true; + } +} diff --git a/modules/qlochannelmanagerconnector/classes/QcmcChannelManagerConnectorDb.php b/modules/qlocmconnector/upgrade/upgrade-5.0.5.php similarity index 50% rename from modules/qlochannelmanagerconnector/classes/QcmcChannelManagerConnectorDb.php rename to modules/qlocmconnector/upgrade/upgrade-5.0.5.php index 470eda17f..8b7b463b9 100644 --- a/modules/qlochannelmanagerconnector/classes/QcmcChannelManagerConnectorDb.php +++ b/modules/qlocmconnector/upgrade/upgrade-5.0.5.php @@ -21,26 +21,44 @@ * @license https://opensource.org/license/osl-3-0-php Open Software License version 3.0 */ -class QcmcChannelManagerConnectorDb + +if (!defined('_PS_VERSION_')) + exit; + +function upgrade_module_5_0_5($module) { - public function createTables() + $objCMConnector = new UpgradeQloCMConnector505($module); + return ( + $objCMConnector->registerHooks() + && $objCMConnector->removeOldModule() + && $objCMConnector->installTab() + ); +} + +class UpgradeQloCMConnector505{ + public function __construct($module) { - $sql = 'CREATE TABLE IF NOT EXISTS `'._DB_PREFIX_.'qcmc_channel_manager_booking` ( - `id_channel_manager_booking` INT(10) UNSIGNED NOT NULL AUTO_INCREMENT, - `id_order` INT(11) UNSIGNED NOT NULL, - `date_add` DATETIME NOT NULL, - PRIMARY KEY (`id_channel_manager_booking`) - ) ENGINE='._MYSQL_ENGINE_.' DEFAULT CHARSET=utf8'; - - if (!Db::getInstance()->execute(trim($sql))) { - return false; - } - - return true; + $this->module = $module; } - public function dropTables() + public function registerHooks() { - return Db::getInstance()->execute('DROP TABLE IF EXISTS '._DB_PREFIX_.'qcmc_channel_manager_booking'); + return $this->module->registerHook( + array( + 'displayAdminListBefore', + 'displayBackOfficeHeader', + 'actionAdminControllerSetMedia', + ) + ); + } + + public function removeOldModule() + { + return $this->module->removeOldChannelManagerConnectorModule(); + } + + public function installTab() + { + return $this->module->callInstallTab(); } } diff --git a/modules/qlochannelmanagerconnector/views/css/admin/admin_tab_logo.css b/modules/qlocmconnector/views/css/admin/admin_tab_logo.css similarity index 100% rename from modules/qlochannelmanagerconnector/views/css/admin/admin_tab_logo.css rename to modules/qlocmconnector/views/css/admin/admin_tab_logo.css diff --git a/modules/qlochannelmanagerconnector/views/img/index.php b/modules/qlocmconnector/views/css/admin/index.php similarity index 100% rename from modules/qlochannelmanagerconnector/views/img/index.php rename to modules/qlocmconnector/views/css/admin/index.php diff --git a/modules/qlochannelmanagerconnector/views/css/admin/wk_cm_info.css b/modules/qlocmconnector/views/css/admin/qcmc_cm_info.css similarity index 96% rename from modules/qlochannelmanagerconnector/views/css/admin/wk_cm_info.css rename to modules/qlocmconnector/views/css/admin/qcmc_cm_info.css index 93fd7121c..cdb5cbd8b 100644 --- a/modules/qlochannelmanagerconnector/views/css/admin/wk_cm_info.css +++ b/modules/qlocmconnector/views/css/admin/qcmc_cm_info.css @@ -92,6 +92,12 @@ line-height: 22px; color: #ff404b; } +.qcm_content_wrapper .connect_txt { + font-weight: 600; + font-size: 16px; + line-height: 22px; + color: #2FA368; +} .qcm_content_wrapper .connect_status_heading { font-weight: 600; font-size: 16px; diff --git a/modules/qlochannelmanagerconnector/views/index.php b/modules/qlocmconnector/views/css/hook/index.php similarity index 100% rename from modules/qlochannelmanagerconnector/views/index.php rename to modules/qlocmconnector/views/css/hook/index.php diff --git a/modules/qlochannelmanagerconnector/views/css/hook/wk_cm_connection_info.css b/modules/qlocmconnector/views/css/hook/qcmc_cm_connection_info.css similarity index 93% rename from modules/qlochannelmanagerconnector/views/css/hook/wk_cm_connection_info.css rename to modules/qlocmconnector/views/css/hook/qcmc_cm_connection_info.css index c3c61f277..59ad6c2ee 100644 --- a/modules/qlochannelmanagerconnector/views/css/hook/wk_cm_connection_info.css +++ b/modules/qlocmconnector/views/css/hook/qcmc_cm_connection_info.css @@ -26,10 +26,6 @@ color: #333; text-align: right; } -.adminqloappschannelmanagerconnector .qcm_booking_title { - font-size: 14px; - margin-bottom: 10px; -} .adminqloappschannelmanagerconnector .channel_info_type { margin-left: 10px; font-weight: bold; @@ -41,6 +37,9 @@ color: #2FA368; font-weight: bold; } +.adminqloappschannelmanagerconnector .channel_connection_status.not_connected { + color: #ff404b; +} .adminqloappschannelmanagerconnector .channel_info_type .icon-info-circle { font-size: 16px; } diff --git a/modules/qlochannelmanagerconnector/views/js/admin/index.php b/modules/qlocmconnector/views/css/index.php similarity index 100% rename from modules/qlochannelmanagerconnector/views/js/admin/index.php rename to modules/qlocmconnector/views/css/index.php diff --git a/modules/qlochannelmanagerconnector/views/img/channel_manager.png b/modules/qlocmconnector/views/img/channel_manager.png similarity index 100% rename from modules/qlochannelmanagerconnector/views/img/channel_manager.png rename to modules/qlocmconnector/views/img/channel_manager.png diff --git a/modules/qlochannelmanagerconnector/views/img/channel_manager_advantage.png b/modules/qlocmconnector/views/img/channel_manager_advantage.png similarity index 100% rename from modules/qlochannelmanagerconnector/views/img/channel_manager_advantage.png rename to modules/qlocmconnector/views/img/channel_manager_advantage.png diff --git a/modules/qlochannelmanagerconnector/views/img/channel_manager_connect.png b/modules/qlocmconnector/views/img/channel_manager_connect.png similarity index 100% rename from modules/qlochannelmanagerconnector/views/img/channel_manager_connect.png rename to modules/qlocmconnector/views/img/channel_manager_connect.png diff --git a/modules/qlochannelmanagerconnector/views/js/index.php b/modules/qlocmconnector/views/img/index.php similarity index 100% rename from modules/qlochannelmanagerconnector/views/js/index.php rename to modules/qlocmconnector/views/img/index.php diff --git a/modules/qlochannelmanagerconnector/views/templates/admin/qloapps_channel_manager_connector/index.php b/modules/qlocmconnector/views/index.php similarity index 100% rename from modules/qlochannelmanagerconnector/views/templates/admin/qloapps_channel_manager_connector/index.php rename to modules/qlocmconnector/views/index.php diff --git a/modules/qlochannelmanagerconnector/views/templates/hook/index.php b/modules/qlocmconnector/views/js/admin/index.php similarity index 99% rename from modules/qlochannelmanagerconnector/views/templates/hook/index.php rename to modules/qlocmconnector/views/js/admin/index.php index 58f2264f2..409280877 100644 --- a/modules/qlochannelmanagerconnector/views/templates/hook/index.php +++ b/modules/qlocmconnector/views/js/admin/index.php @@ -19,7 +19,7 @@ * @author Webkul IN * @copyright Since 2010 Webkul * @license https://opensource.org/license/osl-3-0-php Open Software License version 3.0 - */ +*/ header('Expires: Mon, 26 Jul 1997 05:00:00 GMT'); header('Last-Modified: '.gmdate('D, d M Y H:i:s').' GMT'); diff --git a/modules/qlochannelmanagerconnector/views/js/admin/wk_cm_booking_list.js b/modules/qlocmconnector/views/js/admin/qcmc_cm_booking_list.js similarity index 100% rename from modules/qlochannelmanagerconnector/views/js/admin/wk_cm_booking_list.js rename to modules/qlocmconnector/views/js/admin/qcmc_cm_booking_list.js diff --git a/modules/qlochannelmanagerconnector/classes/index.php b/modules/qlocmconnector/views/js/index.php similarity index 99% rename from modules/qlochannelmanagerconnector/classes/index.php rename to modules/qlocmconnector/views/js/index.php index 58f2264f2..409280877 100644 --- a/modules/qlochannelmanagerconnector/classes/index.php +++ b/modules/qlocmconnector/views/js/index.php @@ -19,7 +19,7 @@ * @author Webkul IN * @copyright Since 2010 Webkul * @license https://opensource.org/license/osl-3-0-php Open Software License version 3.0 - */ +*/ header('Expires: Mon, 26 Jul 1997 05:00:00 GMT'); header('Last-Modified: '.gmdate('D, d M Y H:i:s').' GMT'); diff --git a/modules/qlocmconnector/views/templates/admin/cron_info.tpl b/modules/qlocmconnector/views/templates/admin/cron_info.tpl new file mode 100644 index 000000000..bd9323f31 --- /dev/null +++ b/modules/qlocmconnector/views/templates/admin/cron_info.tpl @@ -0,0 +1,22 @@ +
+
+ + {l s='Cron job setting' mod='qlocmconnector'} +
+ + {if $curl_missing} +
+ {l s='CURL extension is not installed or enabled on your server. Cron execution will not work until CURL is available.' mod='qlocmconnector'} +
+ {/if} + +
+ + {l s='For seamless syncing of availability from the PMS to the Channel Manager automatically, you need to set the following cron job to run every minute' mod='qlocmconnector'} + +

+ + * * * * * curl {$cron_url} + +
+
diff --git a/modules/qlochannelmanagerconnector/controllers/index.php b/modules/qlocmconnector/views/templates/admin/index.php similarity index 99% rename from modules/qlochannelmanagerconnector/controllers/index.php rename to modules/qlocmconnector/views/templates/admin/index.php index 58f2264f2..409280877 100644 --- a/modules/qlochannelmanagerconnector/controllers/index.php +++ b/modules/qlocmconnector/views/templates/admin/index.php @@ -19,7 +19,7 @@ * @author Webkul IN * @copyright Since 2010 Webkul * @license https://opensource.org/license/osl-3-0-php Open Software License version 3.0 - */ +*/ header('Expires: Mon, 26 Jul 1997 05:00:00 GMT'); header('Last-Modified: '.gmdate('D, d M Y H:i:s').' GMT'); diff --git a/modules/qlochannelmanagerconnector/views/templates/admin/index.php b/modules/qlocmconnector/views/templates/admin/qloapps_channel_manager_connector/helpers/index.php similarity index 99% rename from modules/qlochannelmanagerconnector/views/templates/admin/index.php rename to modules/qlocmconnector/views/templates/admin/qloapps_channel_manager_connector/helpers/index.php index 58f2264f2..409280877 100644 --- a/modules/qlochannelmanagerconnector/views/templates/admin/index.php +++ b/modules/qlocmconnector/views/templates/admin/qloapps_channel_manager_connector/helpers/index.php @@ -19,7 +19,7 @@ * @author Webkul IN * @copyright Since 2010 Webkul * @license https://opensource.org/license/osl-3-0-php Open Software License version 3.0 - */ +*/ header('Expires: Mon, 26 Jul 1997 05:00:00 GMT'); header('Last-Modified: '.gmdate('D, d M Y H:i:s').' GMT'); diff --git a/modules/qlocmconnector/views/templates/admin/qloapps_channel_manager_connector/helpers/list/index.php b/modules/qlocmconnector/views/templates/admin/qloapps_channel_manager_connector/helpers/list/index.php new file mode 100644 index 000000000..409280877 --- /dev/null +++ b/modules/qlocmconnector/views/templates/admin/qloapps_channel_manager_connector/helpers/list/index.php @@ -0,0 +1,32 @@ + + {if is_array($title)}{$title|end}{else}{$title}{/if} + {$list_total|intval} + +
+ {l s='Below is the list of all the bookings created by QloApps Channel Manager.' mod='qlocmconnector'} +
+{/block} diff --git a/modules/qlocmconnector/views/templates/admin/qloapps_channel_manager_connector/index.php b/modules/qlocmconnector/views/templates/admin/qloapps_channel_manager_connector/index.php new file mode 100644 index 000000000..409280877 --- /dev/null +++ b/modules/qlocmconnector/views/templates/admin/qloapps_channel_manager_connector/index.php @@ -0,0 +1,32 @@ +
- {l s='Connection status' mod='qlochannelmanagerconnector'} : {l s='Not Connected' mod='qlochannelmanagerconnector'} + {l s='Connection status' mod='qlocmconnector'} : {if $is_cm_connected}{l s='Connected' mod='qlocmconnector'}{else}{l s='Not Connected' mod='qlocmconnector'}{/if}
- {l s='Last updated' mod='qlochannelmanagerconnector'} : {$current_datetime|escape:'htmlall':'UTF-8'} + {l s='Last updated' mod='qlocmconnector'} : {$current_datetime|escape:'htmlall':'UTF-8'}
- **{l s='Connection status with channel manager is showing according to the bookings fetched from QloApps Channel Manager.' mod='qlochannelmanagerconnector'} + **{l s='Connection status with channel manager is showing according to the bookings fetched from QloApps Channel Manager.' mod='qlocmconnector'}
@@ -44,25 +44,25 @@
- +
- {l s='How to connect with channel manager?' mod='qlochannelmanagerconnector'} + {l s='How to connect with channel manager?' mod='qlocmconnector'}
- {l s='You can connect with channel manager through few simple steps' mod='qlochannelmanagerconnector'} : + {l s='You can connect with channel manager through few simple steps' mod='qlocmconnector'} :
    -
  • {l s='Enable QloApps webservice from Webservice tab.' mod='qlochannelmanagerconnector'}
  • -
  • {l s='Create your webservice key and enable all the APIs from Webservice tab.' mod='qlochannelmanagerconnector'}
  • -
  • {l s='Create account on Channel Manager' mod='qlochannelmanagerconnector'} . {l s=' Then enter QloApps webservice credentials under PMS settings of channel manager .' mod='qlochannelmanagerconnector'}
  • -
  • {l s='Synchronize and map QloApps hotels and room types in channel manager.' mod='qlochannelmanagerconnector'}
  • +
  • {l s='Enable QloApps webservice from Webservice tab.' mod='qlocmconnector'}
  • +
  • {l s='Create your webservice key and enable all the APIs from Webservice tab.' mod='qlocmconnector'}
  • +
  • {l s='Create account on Channel Manager' mod='qlocmconnector'} . {l s=' Then enter QloApps webservice credentials under PMS settings of channel manager .' mod='qlocmconnector'}
  • +
  • {l s='Synchronize and map QloApps hotels and room types in channel manager.' mod='qlocmconnector'}
- {l s='To read the connection process in detail, visit' mod='qlochannelmanagerconnector'} {l s='Connection with PMS' mod='qlochannelmanagerconnector'} + {l s='To read the connection process in detail, visit' mod='qlocmconnector'} {l s='Connection with PMS' mod='qlocmconnector'}
- {l s='If you are still not connected with channel manager' mod='qlochannelmanagerconnector'} {l s='Connect It Now' mod='qlochannelmanagerconnector'} + {l s='If you are still not connected with channel manager' mod='qlocmconnector'} {l s='Connect It Now' mod='qlocmconnector'}
@@ -72,19 +72,19 @@
- +
- {l s='What is channel manager?' mod='qlochannelmanagerconnector'} + {l s='What is channel manager?' mod='qlocmconnector'}
- {l s='A channel manager is a centralized software that synchronizes availabilities and details of the property across all platforms i.e. online travel agencies (OTA) and other online distribution channels.' mod='qlochannelmanagerconnector'} + {l s='A channel manager is a centralized software that synchronizes availabilities and details of the property across all platforms i.e. online travel agencies (OTA) and other online distribution channels.' mod='qlocmconnector'}
- {l s='QloApps channel manager synchronizes inventory, rates, restrictions with all connected OTA channels and automated bookings sync connected OTAs and QloApps PMS.' mod='qlochannelmanagerconnector'} + {l s='QloApps channel manager synchronizes inventory, rates, restrictions with all connected OTA channels and automated bookings sync connected OTAs and QloApps PMS.' mod='qlocmconnector'}
- {l s='For channel manager information in detail' mod='qlochannelmanagerconnector'} {l s='Explore Channel Manager' mod='qlochannelmanagerconnector'} + {l s='For channel manager information in detail' mod='qlocmconnector'} {l s='Explore Channel Manager' mod='qlocmconnector'}
@@ -96,24 +96,24 @@
- +
- {l s='Advantages of Channel Manager' mod='qlochannelmanagerconnector'} + {l s='Advantages of Channel Manager' mod='qlocmconnector'}
    -
  • {l s='Hoteliers need not to worry about problems like over-bookings, inconsistent inventory management, and missed opportunities.' mod='qlochannelmanagerconnector'}
  • -
  • {l s='Real-time sync with the world\'s most popular OTA channels like Booking, MakeMyTrip, Expedia, Airbnb, Agoda, Google Hotels, and many more.' mod='qlochannelmanagerconnector'}
  • -
  • {l s='Provides an intuitive & analytical dashboard that brings useful data and stats.' mod='qlochannelmanagerconnector'}
  • -
  • {l s='With the one-click rate and inventory updates, directly push availability and rates on the connected OTA channels.' mod='qlochannelmanagerconnector'}
  • -
  • {l s='Already integrated with QloApps PMS that auto-synchronize inventories for the bookings created on QloApps PMS and connected OTA channels.' mod='qlochannelmanagerconnector'}
  • -
  • {l s='Getting more property impressions by travellers on the world’s leading Online Travel Agencies as well as on the QloApps Hotel booking website will boost your online brand visibility and returns you more reservations.' mod='qlochannelmanagerconnector'}
  • +
  • {l s='Hoteliers need not to worry about problems like over-bookings, inconsistent inventory management, and missed opportunities.' mod='qlocmconnector'}
  • +
  • {l s='Real-time sync with the world\'s most popular OTA channels like Booking, MakeMyTrip, Expedia, Airbnb, Agoda, Google Hotels, and many more.' mod='qlocmconnector'}
  • +
  • {l s='Provides an intuitive & analytical dashboard that brings useful data and stats.' mod='qlocmconnector'}
  • +
  • {l s='With the one-click rate and inventory updates, directly push availability and rates on the connected OTA channels.' mod='qlocmconnector'}
  • +
  • {l s='Already integrated with QloApps PMS that auto-synchronize inventories for the bookings created on QloApps PMS and connected OTA channels.' mod='qlocmconnector'}
  • +
  • {l s='Getting more property impressions by travellers on the world’s leading Online Travel Agencies as well as on the QloApps Hotel booking website will boost your online brand visibility and returns you more reservations.' mod='qlocmconnector'}
- {l s='To know channel manager freatures and work-flow in details' mod='qlochannelmanagerconnector'} {l s='Visit Documentation' mod='qlochannelmanagerconnector'} + {l s='To know channel manager freatures and work-flow in details' mod='qlocmconnector'} {l s='Visit Documentation' mod='qlocmconnector'}
diff --git a/modules/qlocmconnector/views/templates/hook/index.php b/modules/qlocmconnector/views/templates/hook/index.php new file mode 100644 index 000000000..409280877 --- /dev/null +++ b/modules/qlocmconnector/views/templates/hook/index.php @@ -0,0 +1,32 @@ + -
- -
-
- {l s='Connection status' mod='qlochannelmanagerconnector'} : {l s='Connected' mod='qlochannelmanagerconnector'} - {l s='Last updated' mod='qlochannelmanagerconnector'} : {$last_booking_datetime|escape:'htmlall':'UTF-8'} - +
+ {l s='Connection status' mod='qlocmconnector'} : {if $is_cm_connected}{l s='Connected' mod='qlocmconnector'}{else}{l s='Not Connected' mod='qlocmconnector'}{/if} + {l s='Last updated' mod='qlocmconnector'} : {if $last_booking_datetime}{$last_booking_datetime|escape:'htmlall':'UTF-8'}{else}--{/if} +
diff --git a/modules/qlocmconnector/views/templates/index.php b/modules/qlocmconnector/views/templates/index.php new file mode 100644 index 000000000..409280877 --- /dev/null +++ b/modules/qlocmconnector/views/templates/index.php @@ -0,0 +1,32 @@ +