Added: google hotel connector module in the default QloApps

This commit is contained in:
abhishek kumar
2026-09-22 10:51:03 +05:30
parent de6ecd72f0
commit 813c362ebf
55 changed files with 7378 additions and 0 deletions

View File

@@ -722,6 +722,7 @@ class InstallModelInstall extends InstallAbstractModel
'blockcart',
'blockuserinfo',
'qlochannelmanagerconnector',
'qlogooglehotelconnector'
);
}
return $modules;

View File

@@ -0,0 +1,3 @@
---------------------------------------------------------------
# V1.0.0 | COMPATIBLE:: QloApps V1.8.x
---------------------------------------------------------------

View File

@@ -0,0 +1,80 @@
1. Definitions
--------------
"Webkul" means Webkul Software Pvt. Ltd.
"Customization" means those services by which functionality / features are added to a product that are not included in the base installation of a product, which are aimed to suit the specific needs of the customer.
"Module" means the custom add-on module to be developed and provided by Webkul to you in accordance with this policy and the Order Form.
"Order Form" means the order form (electronic or hard copy), accepted by Webkul, through which you ordered the Module to be developed and provided by Webkul and which sets out the details, specifications and parameters of the customized Module to be provided.
"Software" means the software licensed by Webkul to you pursuant to the terms and conditions of a Webkul Software License Agreement.
2. Module Development
---------------------
All customization Services and use of the Module are subject to the terms and conditions of Webkul’s Software Licence Agreement, found on Webkul’s website, and are also subject to the terms and conditions set forth in the Order Form, as well as the customization parameters. No other customization services will be provided unless expressly agreed upon by you and Webkul in writing, signed by authorized representatives of you and Webkul.
3. Customization Services
-------------------------
Customization Services will be provided in accordance with the terms of the Order Form and subject to the conditions set forth on the Order Form including full payment of all fees and adherence to all time limits and scheduling restrictions.
If expressly stated on the Order Form, the Customization Services and Module shall be subject to acceptance by you in accordance with the acceptance criteria set forth in the Order Form. Where there is no acceptance criteria set forth on the Order Form, acceptance shall be deemed to occur upon the earliest of:
completion of the acceptance criteria,
Webkul's completion of the Customization Services and written notification to you of such completion,
your use of the Module in other than a test environment.
Notwithstanding the foregoing, if acceptance testing, delivery or completion is delayed by thirty (30) days or more for reasons not related to Webkul’s performance, you must pay the value of any milestone that is contingent on acceptance. Such acceptance does not negate any warranty rights that you may have respecting the Module, as set forth herein. You are solely responsible for preparing your facilities and equipment for installation of the Module, where applicable.
Webkul will make reasonable efforts to accommodate your time and deadline requirements with respect to the performance of Customization Services; however, meeting such requirements is subject to availability of resources, both human and equipment at the time of your request and is dependent on your availability to provide input and to perform necessary actions. Webkul shall not be liable in any way for any delay or damage arising from Webkul’s failure to meet such of your requirements or any deadlines.
Any changes to the Customization Services or Module requirements after execution of the Order Form will be subject to a cost and delivery re-assessment, and a written change order or re-quote that has been mutually agreed upon by you and Webkul in writing. Webkul reserves the right to make changes in the design of its Software without the obligation to make equivalent changes to the Module.
If, pursuant to your request, Webkul reviews drawings and documents prepared by others in connection with the Customization Services, including your personnel and other consultants, contractors, and suppliers, Webkul’s review of such drawings and documents shall be only to confirm general compliance with the intent of the design and information given, and shall not constitute acceptance by Webkul of any responsibility for correctness of specifications or details of such drawings and documentation. Webkul shall be entitled to rely on all information provided, and decisions and approvals made, by you in connection with Webkul’s work hereunder. You agree that Webkul and its personnel shall not be subject to any liability or costs relating to the Customization Services to the extent such liability and costs are attributable to any information provided by you that is not complete, accurate or current in all material respects.
The Module, and all parts thereof, shall at all times be treated as Software licensed to you under the terms and conditions of the Licence Agreement.
4. Term and Termination
-----------------------
The term of the Development Services, if any, shall be as set out on the Order Form.
Customization Services may be terminated by Webkul immediately upon notice for cause if:
you materially breach any agreement entered into between you and Webkul, including Webkul’s Licence Agreement and/or any User / software license agreement relating to Webkul’s products (or you consistently fail to properly perform and observe your obligations under such agreements or any applicable Webkul Software policy), and you fail to rectify the situation within ten (10) calendar days of notice from Webkul; or
you become insolvent, or a receiver or receiver-manager is appointed for any part of your property, or you make an assignment, proposal or arrangement for the benefit of your creditors or you file an assignment in bankruptcy, or any proceedings under any bankruptcy or insolvency laws are commenced against you.
Webkul shall have the right to terminate Customization Services immediately upon notice to you, without penalty or refund, if any agreement relating to the Software is terminated for any reason.
Each party shall have the right to terminate the Development Services for convenience upon ninety (90) days’ notice to the other party.
Termination of the Customization Services or any agreement shall not affect your payment obligation for any Customization Services performed by Webkul prior to the date of termination, regardless of whether you have had, as at the date of termination, any use of the Module or part thereof. In the event that the Customization Services are terminated prior to the delivery of the Module to you, Webkul shall not be obligated to deliver any work-in-progress or any portion of the Module to you, unless otherwise expressly agreed by the parties in writing.
5. Fees
-------
You shall pay fees for the Customization Services and Module license in accordance with the fee schedule set forth in the Order Form. Webkul shall submit invoices to you for all fees and payments due, including applicable expenses, on a monthly basis in arrears. You shall pay to Webkul the full amount of each invoice received from Webkul within seven (7) days of receipt of the invoice. All invoices shall detail the nature of the Customization Services performed, the fees payable, and the basis on which the calculation of the fees has been made.
You are responsible for, and shall pay all taxes relating to the Customization Services and the Module. Unless otherwise indicated, all amounts payable by you under an Order Form are exclusive of any tax, duty, levy, or similar government charge that may be assessed by any jurisdiction, whether based on gross revenue, the delivery, possession or use of the Module, other Software or Customization Services, or otherwise.
If you fail to pay any amount due within thirty (30) days of such payment becoming due and payable, in addition to any other rights and remedies available to Webkul, Webkul shall be entitled to charge interest on all outstanding amounts at the lesser of 18% per annum or the maximum rate permitted by law, such interest commencing as of the due date for such payment. You shall also be responsible for paying for all reasonable fees and costs incurred by Webkul, including legal fees, in collecting any overdue amounts or enforcing any provision of any agreement entered into between you and Webkul.
Pricing is subject to Webkul’s understanding of the project requirements, the terms of this policy and the Order Form. Webkul reserves the right to invoice you for additional Customization Services requested by you, or that are required based on incomplete details or specifications provided by you, that are not specified in the Order Form but are provided by Webkul to you, provided that Webkul provides you with notice that such additional Customization Services are not included in the Order Form and are subject to Webkul’s then current time and materials fee schedule for such Customization Services. Invoices for such additional Customization Services shall be subject to payment in accordance with this Section.
6. Intellectual Property Rights
-------------------------------
Webkul shall own all intellectual property rights (whether or not patentable or registrable under copyright, trade-mark or similar legislation or subject to analogous protection) in and to the Module, and all work conceived, created, invented, produced, designed or reduced to practice by Webkul and its personnel as a result of or with respect to any and all Customization Services provided to you. Your rights and obligations relating to the use of the Module shall be governed by the terms of the applicable Webkul Software License Agreement regardless of whether you may have contributed to any Module in any way. The foregoing shall not be deemed to transfer ownership to Webkul of any pre-existing intellectual property rights that you may have in the materials that you provide to Webkul in order to permit Webkul to perform the Customization Services; however, to the extent that you have provided such materials to Webkul, you hereby grant Webkul a non-exclusive, non-transferable, royalty-free license to use all such materials for the purposes of performing the Customization Services and providing you with the Module.
7. Limited Warranty and Limitation of Liability
-----------------------------------------------
Limited Warranty: Webkul warrants that all services provided by Webkul shall be provided in a competent, professional manner by persons who are fully trained and qualified to perform the Customization Services. Webkul does not represent or warrant that the Customization Services or Module provided hereunder will be capable of achieving a particular result for your business, or that the operation of the Module will be error free or uninterrupted, or that all errors in the Module can be found or corrected, although Webkul shall use commercially reasonable efforts to do so. Without limiting the foregoing, this warranty is valid only for the six (6) months following the acceptance date as set forth in Section 3(b). The above warranty shall not apply to defects or non-conformities resulting from:
improper or inadequate maintenance or installation of the Module
use of the Module in combination with software, interfaces, or other materials that are not supplied or specifically authorized by Webkul,
unauthorized or improper use or modification of the Module, including use that is not contemplated in the Order Form or instructions provided by Webkul,
abuse, negligence, accident, or other damage from external sources, or
improper preparation of your facilities or equipment for installation and use or the Module.
Limitation of Liability:
save as otherwise provided in a written agreement between you and Webkul, and to the maximum extent permitted by applicable law, Webkul makes no warranty or condition, express or implied, statutory or otherwise, with respect to the module or the Customization services, including, without limitation, the implied warranties or conditions of merchantability and fitness for a particular purpose.
in no event shall Webkul be liable to you or any other person for any indirect, special, punitive, exemplary, consequential or incidental damages (including without limitation, damages for loss of revenues or profits, business interruption, loss of business information, and the like) arising out of the use, inability to use or the performance or non-performance of, the module or the provision of the Customization services, even if Webkul has been advised of the possibility of such damage or claim, or it is foreseeable.
in no event shall Webkul’s maximum aggregate liability to you for direct damages exceed the total amount paid by you for the Customization services performed by Webkul within the six (6) months preceding the date on which the claim arose.
8. General Terms and Conditions
-------------------------------
As Webkul is primarily a plugin and app development company and its main focus is on constant plugin development fulfilling various latest requirements and keeping up with the latest trends, Webkul has all the right to release the same customized plugin as a new plugin in future to be available to all its customers. The same may include few or every feature of the customisation. Webkul reserve all the rights to do the same as the customisation done for the client is Intellectual property of Webkul.
The customization undertaken by Webkul would be identical to the discussed requirements, and would not include any sort of additional changes or alterations. Any additional changes or alterations would be considered as a separate project and would be charged separately.
Webkul would be providing customization support for only one month from the delivery of the project. For any further support extended to you, the same shall be charged separately which includes specifically any further help or bug /error fix.
The cost of customization doesn’t include the cost of extension installation or exchange of the extension, if it has not been specifically quoted within the project cost.
During the project customization, you ought not to edit or change any of the files and it is also advisable to have a backup every-time a change is made. If you, on your own, are making certain changes, it is advisable to back up the files, folder and database. Before doing anything on your own, you need to inform Webkul if Webkul is working on the project or your support period is subsisting.
The customization charge will include a single template. Any time during or after the customization if you change the template, you shall be wholly and solely liable for any loss or damage emanating from the same. Webkul will not be responsible for any loss or damage done due to the same.
All the communication will take place through Webkul’s help desk system only. If the need arises, the issues can be discussed via Skype once in week or month. Webkul does not allow screen sharing and remote desktop.
Customization cost will not include any free consulting beyond the scope of the project.
If while making any changes by yourself or by your team, something gets damaged, then Webkul will not be responsible for it. This includes any issues faced by you for your own fault even during the support period of the module or project.
Webkul will not be responsible for any damages during the customization. It is advised that you must have a proper backup management system deployed.
The terms and conditions of this Policy shall be governed by Indian Laws, and the courts in Delhi alone shall only have exclusive jurisdiction to resolve/ adjudicate upon any disputes.
If any dispute arises between webkul and you/Licensee at any time, in connection with the validity, interpretation, implementation or alleged breach of any provision of this Agreement, the same shall be referred to a sole Arbitrator who shall be an independent and neutral third party appointed exclusively by Webkul. You shall not object to the appointment of the Arbitrator so appointed by Webkul. The place of arbitration shall be Delhi, India. The Arbitration & Conciliation Act, 1996 as amended by The Arbitration & Conciliation (Amendment) Act, 2015, shall govern the arbitration proceedings. The arbitration proceedings shall be held in the English language.

View File

@@ -0,0 +1,223 @@
SOFTWARE LICENCE AGREEMENT
===============================
This AGREEMENT is made effective on the date of the purchase of the software between Webkul Software Pvt. Ltd.,Company incorporated under the Companies Act, 1956 (hereinafter referred to as "Licensor"), and the purchaser of the software/ product (hereinafter referred to as "Licensee").
Preamble
==================
Licensor is a web and mobile product based organization engaged in the business of developing and marketing software for enterprise level e-commerce businesses. It is an ISO and NSR (NASSCOM) certified organization having a team of more than 150 creative engineers which come from different backgrounds. It has developed more than 700 web extensions and apps in the past few years for open source platforms which are used and trusted globally. Licensee now wishes to obtain license, and Licensor wishes to grant a license, to allow use of the software so purchased in developing the e-commerce business website/ mobile app of the Licensee, subject to the terms and conditions set forth herein.
THEREFORE, with the intent to be legally bound, the parties hereby agree as follows:
Agreement
================
1.Definitions :
------------------
As used in this Agreement, the following capitalized terms shall have the definitions set forth below:
"Derivative Works" are works developed by Licensee, its officers, agents, contractors or employees, which are based upon, in whole or in part, the Source Code and/or the Documentation and may also be based upon and/or incorporate one or more other preexisting works of the Licensor. Derivative Works may be any improvement, revision, modification, translation (including compilation or recapitulation by computer), abridgment, condensation, expansion, or any other form in which such a preexisting work may be recast, transformed, or adapted. For purposes hereof, a Derivative Work shall also include any compilation that incorporates such a preexisting work.
"Documentation" is written, printed or otherwise recorded or stored (digital or paper) material relating to the Software and/or Source Code, including technical specifications and instructions for its use including Software/ Source Code annotations and other descriptions of the principles of its operation and instructions for its use.
"Improvements" shall mean, with respect to the Software, all modifications and changes made, developed, acquired or conceived after the date hereof and during the entire term of this Agreement.
"Source Code" is the computer programming source code form of the Software in the form maintained by the Licensor, and includes all non-third-party executables, libraries, components, and Documentation created or used in the creation, development, maintenance, and support of the Software as well as all updates, error corrections and revisions thereto provided by Licensor, in whole or in part.
2. Software License.
-------------------------
(a)Grant of License. For the consideration set forth below, Licensor hereby grants to Licensee, and Licensee hereby accepts the worldwide, non-exclusive, perpetual, royalty-free rights and licenses set forth below:
(i)The right and license to use and incorporate the software, in whole or in part, to develop its website/ mobile app (including the integration of all or part of the Licensor's software into Licensee's own software) on one domain ( Except Joomla modules , listed on store are entitled to be used on unlimited domain as per the standard guidelines ) only, solely for the own personal or business use of the Licensee. However, the License does not authorize the Licensee to compile, copy or distribute the said Software or its Derivative Works.
(ii)The right and license does not authorize the Licensee to make any backup or archival copies of the Software and / or the Source Code and Documentation.
(iii)The right and license does not authorize the Licensee to migrate the domain license to another domain.
(b)Scope; Rights and Responsibilities.
(i)Licensor shall enable the Licensee to download one complete copy of the Software.
(ii)The Software is intended for the sole use of the Licensee in development of its own website/ mobile app.
(iii)Licensee does not have the right to hand over, sell, distribute, sub-license, rent, lease or lend any portion of the Software or Documentation, whether modified or unmodified, to anyone. Licensee should not place the Software on a server so that it becomes accessible via a public network such as the Internet for distribution purposes. In case the Licensee is using any source code management system like github, it can use the code there only when it has paid subscription from such management system.
(iv)Licensee is not authorized to appoint, or work with, third parties to perform any development services using the Source Code, the source code to Derivative Works and/or the Documentation on behalf of, or working with, the Licensee. Release of Source Code, Derivative Work source code and/or Documentation to any third party shall be considered as violation of the Agreement, inter-alia entailing forthwith termination and legal action.
(c)Ownership.
(i)Software and Source Code. All right, title, copyright, and interest in the Software, Source Code, Software Modifications and Error corrections will be and remain the property of Licensor.
(ii)Derivative Works. As creation of Derivative Works by the Licensee is prohibited, thus, all right, title, copyright, and interest in any and/or all Derivative Works and Improvements created by, or on behalf of, Licensee will also be deemed to the property of Licensor. Licensor shall be entitled to protect copyright / intellectual property in all such Derivative Works and Improvements also in any country as it may deem fit including without limitation seeking copyright and/or patent protection.
3.Consideration.
--------------------
(a)Licensee shall pay to Licensor the amount as mentioned on the website from where the order is placed, as one-time, upfront fees in consideration for the licenses and rights granted hereunder (hereinafter referred to as the "License Fee"). The License Fee to be paid by Licensee shall be paid upfront at the time of placing the order, and no credit will be allowed under any circumstances.
(b)Once paid, the License Fees shall be non-refundable. The Licensee has fully satisfied itself about the Software and has seen the demonstration, and only thereafter has placed the order. Thus, the License Fees or any part thereof is non-refundable. No claim for refund of the Licence Fees shall be entitled to seek any refund of the amount paid by it to the Licensor, under any circumstances.
4.Representations and Warranties.
-------------------------------------
(a)Mutual. Each of the parties represents and warrants to the other as follows.
(i)such party is a legal entity duly organized, validly existing and in good standing;
(ii)such party has the power and authority to conduct its business as presently conducted and to enter into, execute, deliver and perform this Agreement.
(iii)This Agreement has been duly and validly accepted by such party and constitutes the legal, valid and binding obligations of such party respectively, enforceable against such party in accordance with their respective terms;
(iv)the acceptance, execution, delivery and performance of this Agreement does not and will not violate such party's charter or by-laws; nor require any consent, authorization, approval, exemption or other action by any third party or governmental entity.
(b)Licensor warrants that, at the time of purchase of the Software:
the Software will function materially as set forth in the website or published functionality provided by Licensor to customers and potential customers describing the Software; and
Software add-ons, if purchased by the Licensee from the Licensor, will not materially diminish the features or functions of or the specifications of the Software as they existed as of the execution of this Agreement.
(c)Title. Licensor represents and warrants that it is the exclusive owner of all copyright/ intellectual property in the Software (including the Source Code) and has good and marketable title to the Software (including the Source Code) free and clear of all liens, claims and encumbrances of any nature whatsoever (collectively, "Liens"). Licensor's grant of license and rights to Licensee hereunder does not, and will not infringe any third party's property, intellectual property or personal rights.
5.Term.
-------------
(a)Subject to Licensee's payment obligations, this Agreement shall commence as on the date of making payment of the Software by the Licensee to the Licensor, and shall continue until terminated by either party.
(b)The Licensor retains the right to terminate the license at any time, if the Licensee is not abiding by any of the terms of the Agreement. The Licensee may terminate the Agreement at any time at its own discretion by uninstalling the Software and /or by destroying the said Software (or any copies thereof). However, the Licensee shall not be entitled to seek any refund of the amount paid by it to the Licensor, under any circumstances.
(c)Survival. In the event this Agreement is terminated for any reason, the provisions set forth in Sections 2(a), 2(b), and 2(c) shall survive.
6. Indemnification
----------------------
The Licensee release the Licensor from, and agree to indemnify, defend and hold harmless the Licensor (and its officers, directors, employees, agents and Affiliates) against, any claim, loss, damage, settlement, cost, taxes, expense or other liability (including, without limitation, attorneys' fees) (each, a "Claim") arising from or related to: (a) any actual or alleged breach of any obligations in this Agreement; (b) any refund, adjustment, or return of Software,(c) any claim for actual or alleged infringement of any Intellectual Property Rights made by any third party or damages related thereto; or (d) Taxes.
7. Limitation of Liability
------------------------------
The Licensor will not be liable for any direct, indirect, incidental, special, consequential or exemplary damages, including but not limited to, damages for loss of profits, goodwill, use, data or other intangible losses arising out of or in connection with the Software, whether in contract, warranty, tort etc. (including negligence, software liability, any type of civil responsibility or other theory or otherwise) to the Licensee or any other person for cost of software, cover, recovery or recoupment of any investment made by the Licensee or its affiliates in connection with this Agreement, or for any other loss of profit, revenue, business, or data or punitive or consequential damages arising out of or relating to this Agreement. Further, the aggregate liability of the Licensor, arising out of or in connection with this Agreement or the transactions contemplated hereby will not exceed at any time, or under any circumstances, the total amounts received by the Licensor from the Licensee in connection with the particular software giving rise to the claim.
8. Force Majeure
---------------------
The Licensor will not be liable for any delay or failure to perform any of its obligations under this Agreement by reasons, events or other matters beyond its reasonable control.
9. Relationship of Parties
------------------------------
The Licensor and Licensee are independent legal entities, and nothing in this Agreement will be construed to create a partnership, joint venture, association of persons, agency, franchise, sales representative, or employment relationship between the parties. The Licensee will have no authority to make or accept any offers or representations on behalf of the Licensor. The relationship between the parties is that of Licensor and Licensee only, and the rights, duties, liabilities of each party shall be governed by this Agreement.
10. Modification
------------------------
The Licensor may amend any of the terms and conditions contained in this Agreement at any time and solely at its discretion. Any changes will be effective upon the posting of such changes on the Portal/ website, and the Licensee is responsible for reviewing these changes and informing itself of all applicable changes or notices. The continued use of a software by the Licensee after posting of any changes by the Licensor, will constitute the acceptance of such changes or modifications by the Licensee.
11.Miscellaneous.
-----------------------
(a)General Provisions. This Agreement: (i) may be amended only by a writing signed by each of the parties; (ii) may be executed in several counterparts, each of which shall be deemed an original but all of which shall constitute one and the same instrument; (iii) contains the entire agreement of the parties with respect to the transactions contemplated hereby and supersedes all prior written and oral agreements, and all contemporaneous oral agreements, relating to such transactions; (iv) shall be governed by, and construed and enforced in accordance with, the laws of India; and (v) shall be binding upon, and inure to the benefit of, the parties and their respective successors and permitted assigns. Each of the parties hereby irrevocably submits to the jurisdiction of the Courts at Delhi, India, for the purposes of any action or proceeding arising out of or relating to this Agreement or the subject matter hereof and brought by any other party.
(b)Assignment. Licensee cannot assign, pledge or otherwise transfer, whether by operation of law or otherwise, this Agreement, or any of its obligations hereunder, without the prior written consent of Licensor, which consent shall not be unreasonably withheld.
(c)Notices. Unless otherwise specifically provided herein, all notices, consents, requests, demands and other communications required or permitted hereunder:
(i)shall be in writing;
(ii)shall be sent by messenger, certified or registered mail/email, or reliable express delivery service, to the appropriate address(es) set forth below; and
(iii)shall be deemed to have been given on the date of receipt by the addressee, as evidenced by a receipt executed by the addressee (or a responsible person in his or her office), the records of the Party delivering such communication or a notice to the effect that such addressee refused to claim or accept such communication, if sent by messenger, mail or express delivery service.
All such communications shall be sent to the following addresses or numbers, or to such other addresses or numbers as any party may inform the others by giving five days' prior notice:
If to Webkul Software Pvt. Ltd.:
Webkul Software Pvt. Ltd.
H-28, ARV Park, Sector 63, NOIDA – 201301,
Uttar Pradesh, India
If to Licensee:
At the address mentioned by the Licensee
(at the time of placing order of generating Invoice)
(d)Severability. It is the intent of the parties that the provisions of this Agreement be enforced to the fullest extent permissible under the laws and public policies of India in which enforcement hereof is sought. In furtherance of the foregoing, each provision hereof shall be severable from each other provision, and any provision hereof which is/ becomes unenforceable shall be subject to the following: (i) if such provision is contrary to or conflicts with any requirement of any statute, rule or regulation in effect, then such requirement shall be incorporated into, or substituted for, such unenforceable provision to the minimum extent necessary to make such provision enforceable; (ii) the court, agency or arbitrator considering the matter is hereby authorized to (or, if such court, agency or arbitrator is unwilling or fails to do so, then the parties shall) amend such provision to the minimum extent necessary to make such provision enforceable, and the parties hereby consent to the entry of an order so amending such provision; and (iii) if any such provision cannot be or is not reformed and made enforceable pursuant to clause (i) or (ii) above, then such provision shall be ineffective to the minimum extent necessary to make the remainder of this Agreement enforceable. Any application of the foregoing provisions to any provision hereof shall not effect the validity or enforceability of any other provision hereof.
(e)By purchasing the Software, the Licensee acknowledge that it has read this Agreement, and that it agrees to the content of the Agreement, its terms and agree to use the Software in compliance with this Agreement.
(f)The Licensor holds the sole copyright of the Software. The Software or any portion thereof is a copyrightable matter and is liable to be protected by the applicable laws. Copyright infringement in any manner can lead to prosecution according to the current law. The Licensor reserves the right to revoke the license of any user who is not holding any license or is holding an invalid license.
(g)This Agreement gives the right to use only one copy of the Software on one domain solely for the own personal or business use of the Licensee, subject to all the terms and conditions of this Agreement. A separate License has to be purchased for each new Software installation. Any distribution of the Software without the written consent of the Licensor (including non-commercial distribution) is regarded as violation of this Agreement, and will entail immediate termination of the Agreement and may invite liability, both civil and criminal, as per applicable laws.
(h)The Licensor reserves the rights to publish a selected list of users/ Licensees of its Software, and no permission of any Licensee is needed in this regard. The Licensee agrees that the Licensor may, in its sole discretion, disclose or make available any information provided or submitted by the Licensee or related to it under this Agreement to any judicial, quasi-judicial, governmental, regulatory or any other authority as may be required by the Licensor to co-operate and / or comply with any of their orders, instructions or directions or to fulfill any requirements under applicable Laws.
(i)If the Licensee continues to use the Software even after the sending of the notice by the Licensor for termination, the Licensee agree to accept an injunction to restrain itself from its further use, and to pay all costs (including but not limited to reasonable attorney fees) to enforce injunction or to revoke the License, and any damages suffered by the Licensor because of the misuse of the Software by the Licensee.
12. Arbitration
-----------------------
If any dispute arises between the Licensor and the Licensee at any time, in connection with the validity, interpretation, implementation or alleged breach of any provision of this Agreement, the same shall be referred to a sole Arbitrator who shall be an independent and neutral third party appointed exclusively by the Licensor. The Licensee shall not object to the appointment of the Arbitrator so appointed by the Licensor. The place of arbitration shall be Delhi, India. The Arbitration & Conciliation Act, 1996 as amended by The Arbitration & Conciliation (Amendment) Act, 2015, shall govern the arbitration proceedings. The arbitration proceedings shall be held in the English language.

View File

@@ -0,0 +1,28 @@
### QloApps Google Hotel Connector
### User Guide Documentation link:
https://qloapps.com/
### Support Policy:
https://store.webkul.com/support.html/
### Store link:
https://store.webkul.com/
### Explore Modules:
https://store.webkul.com/Qloapps.html
### Explore Addons:
https://qloapps.com/addons/
### Module Versions and Compatibility:
- **Current version:** `1.0.0`
- Module version `1.0.0` compatible with QloApps version `1.8.x`
### Refund Policy:
https://store.webkul.com/refund-policy.html/

View File

@@ -0,0 +1,98 @@
<?php
/**
* 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
*/
class QghcApiLog extends ObjectModel
{
const STATUS_SUCCESS = 1;
const STATUS_ERROR = 0;
// `api_type` column values — every producer (QghcApiService, WebserviceSpecificManagementGhcApi)
// and consumer (AdminGoogleHotelLogsController::$typeLabels) must use these, never a raw string,
// or a new type silently shows as its raw value in the admin list (already happened once).
const TYPE_UNKNOWN = 'unknown';
const TYPE_CONNECTION = 'connection';
const TYPE_DELETE_CONNECTION = 'delete_connection';
const TYPE_GOOGLE_STATUS = 'google_status';
const TYPE_HOTEL_LIST_FEED = 'hotel_list_feed';
const TYPE_ARI_FULL = 'ari_full';
const TYPE_ARI_CHANGE = 'ari_change';
public $id_hotel;
public $api_type;
public $request;
public $response;
public $status;
public $message;
public $date_add;
public static $definition = array(
'table' => 'qghc_api_log',
'primary' => 'id_qghc_api_log',
'fields' => array(
'id_hotel' => array('type' => self::TYPE_INT),
'api_type' => array('type' => self::TYPE_STRING, 'size' => 50),
'request' => array('type' => self::TYPE_STRING),
'response' => array('type' => self::TYPE_STRING),
'status' => array('type' => self::TYPE_INT),
'message' => array('type' => self::TYPE_STRING),
'date_add' => array('type' => self::TYPE_DATE),
),
);
/**
* Records one webservice request/response cycle.
* Uses a raw Db::insert() rather than ObjectModel::add() since `request`/`response`
* hold arbitrary-length JSON bodies — add()'s default pSQL($value, false) would run
* strip_tags()/nl2br() on them, mangling any `<`/`>` or newline in the JSON. The
* `true` htmlOK flag below skips that for these two fields.
*
* @param int $idHotel
* @param string $apiType
* @param string $request
* @param string $response
* @param int $status self::STATUS_SUCCESS or self::STATUS_ERROR
* @param string $message
* @return bool
*/
public static function insert($idHotel, $apiType, $request, $response, $status, $message)
{
try {
$inserted = (bool) Db::getInstance()->insert('qghc_api_log', array(
'id_hotel' => (int) $idHotel,
'api_type' => pSQL($apiType),
'request' => pSQL($request, true),
'response' => pSQL($response, true),
'status' => (int) $status,
'message' => pSQL($message),
'date_add' => date('Y-m-d H:i:s'),
));
if (!$inserted) {
error_log('QghcApiLog::insert failed (no exception) for api_type=' . $apiType);
}
return $inserted;
} catch (\Exception $e) {
error_log('QghcApiLog::insert failed: ' . $e->getMessage());
return false;
}
}
}

View File

@@ -0,0 +1,206 @@
<?php
/**
* 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
*/
if (!defined('QGHC_CM_BASE_URL')) {
define('QGHC_CM_BASE_URL', 'https://channels.qloapps.com/');
}
class QghcApiService
{
private $baseUrl = QGHC_CM_BASE_URL;
private $curlConfig = array();
private $apiType = QghcApiLog::TYPE_UNKNOWN;
protected function setApiType($apiType)
{
$this->apiType = $apiType;
}
protected function setMethod($method = 'POST')
{
$this->curlConfig['method'] = strtoupper($method);
}
protected function setEndpoint($endpoint)
{
$this->curlConfig['url'] = rtrim($this->baseUrl, '/') . '/' . ltrim($endpoint, '/');
}
protected function addHeaders(array $headers)
{
$this->curlConfig['headers'] = array_merge(
isset($this->curlConfig['headers']) ? $this->curlConfig['headers'] : array(),
$headers
);
}
protected function setBody($body)
{
$this->curlConfig['body'] = $body;
}
protected 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);
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, true);
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']);
}
$rawResult = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
$curlError = curl_error($ch);
curl_close($ch);
if ($rawResult) {
$result = json_decode($rawResult, true);
if (!is_array($result)) {
$result = array('raw' => $rawResult);
}
} else {
$result = array('curl_error' => $curlError);
}
$result['http_code'] = $httpCode;
$this->saveApiLog($result);
$this->curlConfig = array();
$this->apiType = QghcApiLog::TYPE_UNKNOWN;
return $result;
}
protected function saveApiLog($result)
{
$request = isset($this->curlConfig['body']) ? $this->curlConfig['body'] : '';
$response = json_encode($result, JSON_UNESCAPED_SLASHES);
$httpCode = isset($result['http_code']) ? (int)$result['http_code'] : 0;
$success = $httpCode >= 200 && $httpCode < 300;
// File Log started
$logDir = _PS_MODULE_DIR_ . 'qlogooglehotelconnector/logs/';
if (!is_dir($logDir)) {
@mkdir($logDir, 0755, true);
}
$requestDecoded = json_decode($request, true);
if (is_array($requestDecoded) && isset($requestDecoded['api_key'])) {
$requestDecoded['api_key'] = '[REDACTED]';
}
$requestDisplay = is_array($requestDecoded)
? json_encode($requestDecoded, JSON_UNESCAPED_SLASHES)
: $request;
$headersDisplay = array();
foreach ((array)(isset($this->curlConfig['headers']) ? $this->curlConfig['headers'] : array()) as $h) {
$name = strtolower(strtok($h, ':'));
$headersDisplay[] = in_array($name, array('api-key', 'authorization'))
? strtok($h, ':') . ': [REDACTED]'
: $h;
}
$url = isset($this->curlConfig['url']) ? $this->curlConfig['url'] : '';
$method = isset($this->curlConfig['method']) ? $this->curlConfig['method'] : '';
$data = date('[Y-m-d H:i e] ') . PHP_EOL;
$data .= 'Url : ' . $url . PHP_EOL;
$data .= 'Method : ' . $method . PHP_EOL;
$data .= 'Headers : ' . json_encode($headersDisplay, JSON_UNESCAPED_SLASHES) . PHP_EOL;
$data .= 'Request : ' . $requestDisplay . PHP_EOL;
$data .= 'Response: ' . $response . PHP_EOL;
if (!empty($result['curl_error'])) {
$data .= 'CurlErr : ' . $result['curl_error'] . PHP_EOL;
}
$data .= '___________________________________________';
$data .= PHP_EOL . PHP_EOL;
error_log($data, 3, $logDir . date('Y-m-d') . '.log');
// File Log END
// DB Log started
$message = $success ? '' : (
$httpCode > 0
? 'HTTP ' . $httpCode
: (!empty($result['curl_error']) ? $result['curl_error'] : 'No response')
);
QghcApiLog::insert(
0,
$this->apiType,
$requestDisplay,
$response,
$success ? QghcApiLog::STATUS_SUCCESS : QghcApiLog::STATUS_ERROR,
$message
);
// DB Log END
}
/**
* Registers this PMS with the channel manager.
* Sends our shop URL (pms_url) and our PrestaShop webservice key (api_key)
* so the CM can authenticate future calls to our /api/ghc_api/* endpoints.
*/
public function createConnection($pmsUrl, $apiKey)
{
$this->setApiType(QghcApiLog::TYPE_CONNECTION);
$this->setEndpoint('/api/google-hotel-connection');
$this->setMethod('POST');
$this->addHeaders(array(
'Content-Type: application/json',
'Accept: application/json',
));
$this->setBody(json_encode(array(
'pms_url' => $pmsUrl,
'api_key' => $apiKey,
)));
return $this->getResult();
}
/**
* Deregisters this PMS from the channel manager.
* The CM removes all associated data for this pms_url + api_key pair.
* Local connection state is only cleared after the CM confirms success.
*/
public function deleteConnection($pmsUrl, $apiKey)
{
$this->setApiType(QghcApiLog::TYPE_DELETE_CONNECTION);
$this->setEndpoint('/api/google-hotel-connection');
$this->setMethod('DELETE');
$this->addHeaders(array(
'Content-Type: application/json',
'Accept: application/json',
));
$this->setBody(json_encode(array(
'pms_url' => $pmsUrl,
'api_key' => $apiKey,
)));
return $this->getResult();
}
}

View File

@@ -0,0 +1,511 @@
<?php
/**
* 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
*/
class QghcAri
{
// Returns full ARI for a property — one flat ari_info entry per date segment.
public function getAriByIdProperty($params)
{
$finalPayload = array();
if ($dateWiseHotelAri = $this->getHotelAri($params)) {
foreach ($dateWiseHotelAri as $idHotel => $productWiseAri) {
if (!isset($finalPayload[$idHotel])) {
$finalPayload[$idHotel] = array(
'id_property' => '',
'id_pms_property' => (string)$idHotel,
'ari_info' => array(),
);
}
foreach ($productWiseAri as $idProduct => $dayByDayAri) {
if (empty($dayByDayAri)) {
continue;
}
foreach ($this->buildFlatAriSegments($idProduct, $dayByDayAri) as $segment) {
$finalPayload[$idHotel]['ari_info'][] = $segment;
}
}
}
}
return json_encode(array(
'success' => true,
'data' => array_values($finalPayload),
));
}
// Returns ARI only for changed dates — same flat segment format as getAriByIdProperty.
// Deletes fetched rows after building the response (race-safe via ID capture).
public function getAriChange($params)
{
$idProperty = (int)($params['id_property'] ?? 0);
if (!$idProperty) {
return json_encode(array('success' => false, 'error' => 'id_property is required'));
}
$objAriUpdates = new QghcAriUpdates();
$changedRows = $objAriUpdates->getChangedRows($idProperty);
if (empty($changedRows)) {
return json_encode(array('success' => true, 'data' => array()));
}
$changedIds = array();
$changedDatesByRoomType = array();
foreach ($changedRows as $row) {
$changedIds[] = (int)$row['id_qghc_ari_updates'];
$idRoomType = (int)$row['id_room_type'];
$date = date('Y-m-d', strtotime($row['ari_date']));
$changedDatesByRoomType[$idRoomType][$date] = true;
}
$collectedAri = array();
foreach ($changedDatesByRoomType as $idRoomType => $changedDates) {
ksort($changedDates);
$ranges = $this->buildConsecutiveDateRanges(array_keys($changedDates));
foreach ($ranges as $range) {
$ariParams = array(
'id_property' => $idProperty,
'id_room_type' => (string)$idRoomType,
'date_from' => $range['from'],
'date_to' => date('Y-m-d', strtotime($range['to'] . ' +1 day')),
);
$ariData = $this->getHotelAri($ariParams);
if (!empty($ariData[$idProperty][$idRoomType])) {
if (!isset($collectedAri[$idProperty][$idRoomType])) {
$collectedAri[$idProperty][$idRoomType] = array();
}
foreach ($ariData[$idProperty][$idRoomType] as $dayEntry) {
if (isset($changedDates[$dayEntry['date_from']])) {
$collectedAri[$idProperty][$idRoomType][] = $dayEntry;
}
}
}
}
}
$finalPayload = array();
foreach ($collectedAri as $idHotel => $productWiseAri) {
if (!isset($finalPayload[$idHotel])) {
$finalPayload[$idHotel] = array(
'id_property' => '',
'id_pms_property' => (string)$idHotel,
'ari_info' => array(),
);
}
foreach ($productWiseAri as $idProduct => $dayByDayAri) {
if (empty($dayByDayAri)) {
continue;
}
foreach ($this->buildFlatAriSegments($idProduct, $dayByDayAri) as $segment) {
$finalPayload[$idHotel]['ari_info'][] = $segment;
}
}
}
// Delete only the rows we originally fetched — new changed rows written during
// processing are preserved for the next call.
$objAriUpdates->deleteChangedRowsByIds($changedIds);
return json_encode(array(
'success' => true,
'data' => array_values($finalPayload),
));
}
// Per-date ARI calculation — restricted to room types configured for QGHC.
public function getHotelAri($params)
{
$idProperty = (int)($params['id_property'] ?? 0);
if (!$idProperty) {
return array();
}
$minimumBookingOffset = HotelOrderRestrictDate::getMinimumBookingOffset($idProperty);
$maximumCheckoutOffset = HotelOrderRestrictDate::getMaximumCheckoutOffset($idProperty);
// Room types connected to QGHC for this property — the only ones ever allowed out
// to Google, regardless of what's requested. Changed rows in qghc_ari_updates
// are written by hotel-wide events (e.g. booking-offset changes) that don't check QGHC
// connection, so only_change=1 must re-filter here too, not just the full-sync path.
$connectedProducts = QghcRoomType::getConnectedProductIds($idProperty);
if (!empty($params['id_room_type'])) {
$requested = array_filter(array_map('intval', explode('|', $params['id_room_type'])));
$idProducts = array_values(array_intersect($requested, $connectedProducts));
} else {
$idProducts = $connectedProducts;
}
if (empty($idProducts)) {
return array();
}
// Hotel's own resolved max booking offset (hotel-specific override if set,
// otherwise the global default — see HotelOrderRestrictDate::getMaxOrderDate()).
$maxOrderDate = date('Y-m-d', strtotime(HotelOrderRestrictDate::getMaxOrderDate($idProperty)));
$dateFrom = !empty($params['date_from'])
? $params['date_from']
: date('Y-m-d');
$dateTo = !empty($params['date_to'])
? $params['date_to']
: $maxOrderDate;
// The resolved offset is a hard ceiling, not just a default: dates beyond it
// aren't bookable at this hotel regardless of what the caller explicitly
// requests, so never return ARI past it even for an explicit wider date_to.
if (strtotime($dateTo) > strtotime($maxOrderDate)) {
$dateTo = $maxOrderDate;
}
$bookingParams = array(
'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 = array();
$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);
$rooms = HotelRoomInformation::getHotelRoomsInfo($idProperty, $idProduct);
$fallbackLos = array();
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)));
$ariDateInfo = array(
'date_from' => $currentDate,
'date_to' => $nextDate,
);
$totalBookingPrice = QghcPropertyService::getRoomTypeFinalPriceForSdm(
$idProduct,
$currentDate,
date('Y-m-d H:i:s', strtotime('+1 day', strtotime($currentDate))),
1
);
// LOS restrictions
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 {
if (!isset($fallbackLos[$idProduct])) {
$losRow = Db::getInstance()->getRow(
'SELECT `min_los`, `max_los` FROM `' . _DB_PREFIX_ . 'htl_room_type`
WHERE `id_product` = ' . (int)$idProduct
);
$fallbackLos[$idProduct] = $losRow
? array('min_los' => (int)$losRow['min_los'], 'max_los' => (int)$losRow['max_los'])
: array('min_los' => 1, 'max_los' => 0);
}
$ariDateInfo['min_los'] = $fallbackLos[$idProduct]['min_los'];
$ariDateInfo['max_los'] = $fallbackLos[$idProduct]['max_los'];
}
if ($totalBookingPrice && is_array($totalBookingPrice) && isset($totalBookingPrice['total_price_tax_incl'])) {
$ariDateInfo['total_price_with_tax'] = (float)$totalBookingPrice['total_price_tax_incl'];
$ariDateInfo['total_price_tax_excl'] = (float)$totalBookingPrice['total_price_tax_excl'];
$ariDateInfo['tax'] = (float)$totalBookingPrice['total_price_tax_incl'] - (float)$totalBookingPrice['total_price_tax_excl'];
} else {
$ariDateInfo['total_price_with_tax'] = 0.0;
$ariDateInfo['total_price_tax_excl'] = 0.0;
$ariDateInfo['tax'] = 0.0;
}
$ariDateInfo['minimum_booking_offset'] = $minimumBookingOffset;
$ariDateInfo['maximum_booking_offset'] = $maximumCheckoutOffset;
if (!empty($bookingData['rm_data'])) {
foreach ($bookingData['rm_data'] as $roomType) {
$ariDateInfo['total_available_rooms'] = $roomType['numberOfRooms'];
// Booked rooms
$bookedRoomIds = array();
if (isset($roomType['data']['booked'])) {
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'];
}
}
}
}
}
$bookedRooms = array_values(array_filter($rooms, function($room) use ($bookedRoomIds) {
return in_array($room['id'], $bookedRoomIds);
}));
$ariDateInfo['total_available_rooms'] -= count($bookedRooms);
// Unavailable rooms
$unavailableRoomIds = array();
if (isset($roomType['data']['unavailable'])) {
foreach ($roomType['data']['unavailable'] as $unavailRow) {
if (!empty($unavailRow['detail'])) {
foreach ($unavailRow['detail'] as $unavailDetail) {
// Skip LOS-restriction mismatch (status 4) — it is a booking
// search filter, not a physical room block. Counting it would
// zero-out availability whenever the ARI query range is wider
// than the room type's max_los (e.g. full-year ARI sync).
if (HotelRoomInformation::STATUS_SEARCH_LOS_UNSATISFIED === (int)$unavailDetail['id_status']) {
continue;
}
if (!$unavailDetail['date_from'] || !$unavailDetail['date_to']) {
$unavailableRoomIds[] = $unavailRow['id_room'];
} elseif (
$currentDate >= date('Y-m-d', strtotime($unavailDetail['date_from']))
&& $currentDate < date('Y-m-d', strtotime($unavailDetail['date_to']))
) {
$unavailableRoomIds[] = $unavailRow['id_room'];
}
}
}
}
}
$unavailableRooms = array_values(array_filter($rooms, function($room) use ($unavailableRoomIds) {
return in_array($room['id'], $unavailableRoomIds);
}));
$ariDateInfo['total_available_rooms'] -= count($unavailableRooms);
$ariDateInfo['total_available_rooms'] = max(0, $ariDateInfo['total_available_rooms']);
}
} else {
$ariDateInfo['total_available_rooms'] = 0;
}
$searchAriData[$idProperty][$idProduct][] = $ariDateInfo;
}
}
return $searchAriData;
}
public function getDateWiseRestrictionOfProducts($idProducts, $dateFrom, $dateTo)
{
$losArr = array();
foreach ($idProducts as $idProduct) {
if (!isset($losArr[$idProduct])) {
$losArr[$idProduct] = array();
}
// Load room type by id_product (NOT by primary key `id` — they differ).
$roomTypeRow = Db::getInstance()->getRow(
'SELECT `min_los`, `max_los` FROM `' . _DB_PREFIX_ . 'htl_room_type`
WHERE `id_product` = ' . (int)$idProduct
);
if ($roomTypeRow) {
$minLosDefault = (int)$roomTypeRow['min_los'];
$maxLosDefault = (int)$roomTypeRow['max_los'];
$objRestrRange = new HotelRoomTypeRestrictionDateRange();
$lengthStayArr = $objRestrRange->getRoomTypeLengthOfStayRestriction($idProduct);
// Pre-index restrictions by date for O(1) lookup
$restrictionByDate = array();
foreach ($lengthStayArr as $restriction) {
for (
$restrDate = $restriction['date_from'];
$restrDate <= $restriction['date_to'];
$restrDate = date('Y-m-d', strtotime('+1 day', strtotime($restrDate)))
) {
if (!isset($restrictionByDate[$restrDate])) {
$restrictionByDate[$restrDate] = array(
'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,
);
}
}
}
for (
$currentDate = $dateFrom;
$currentDate < $dateTo;
$currentDate = date('Y-m-d', strtotime('+1 day', strtotime($currentDate)))
) {
$losArr[$idProduct][$currentDate] = isset($restrictionByDate[$currentDate])
? $restrictionByDate[$currentDate]
: array('min_los' => $minLosDefault, 'max_los' => $maxLosDefault);
}
}
}
return $losArr;
}
// Splits day-by-day ARI for one room type into flat ari_info segments.
// A new segment starts when availability, price, LOS, or booking offset changes.
// Returns an array of complete ari_info entries ready to append to the payload.
private function buildFlatAriSegments($idProduct, $dayByDayAri)
{
usort($dayByDayAri, function($a, $b) {
return strcmp($a['date_from'], $b['date_from']);
});
$segments = array();
$cur = null;
foreach ($dayByDayAri as $day) {
if ($cur === null) {
$cur = $this->openSegment($idProduct, $day);
} elseif (
$cur['_to'] === $day['date_from']
&& $cur['availability'] === (int)$day['total_available_rooms']
&& $cur['_price'] == (float)$day['total_price_with_tax']
&& $cur['_min_los'] === (int)$day['min_los']
&& $cur['_max_los'] === (int)$day['max_los']
&& $cur['_min_off'] === (int)$day['minimum_booking_offset']
&& $cur['_max_off'] === (int)$day['maximum_booking_offset']
) {
$cur['date_to'] = $day['date_to'];
$cur['_to'] = $day['date_to'];
} else {
$segments[] = $this->closeSegment($cur);
$cur = $this->openSegment($idProduct, $day);
}
}
if ($cur !== null) {
$segments[] = $this->closeSegment($cur);
}
return $segments;
}
// Opens a new segment starting at $day, storing merge-state fields prefixed with _.
private function openSegment($idProduct, $day)
{
return array(
'id_room_type' => '',
'id_pms_room_type' => (string)$idProduct,
'date_from' => $day['date_from'],
'date_to' => $day['date_to'],
'weekdays' => array('sun', 'mon', 'tue', 'wed', 'thu', 'fri', 'sat'),
'availability' => (int)$day['total_available_rooms'],
'closed' => 0,
'rates' => array($this->buildRateObject($day)),
// Internal merge-state fields — stripped before output
'_to' => $day['date_to'],
'_price' => (float)$day['total_price_with_tax'],
'_min_los' => (int)$day['min_los'],
'_max_los' => (int)$day['max_los'],
'_min_off' => (int)$day['minimum_booking_offset'],
'_max_off' => (int)$day['maximum_booking_offset'],
);
}
// Strips internal merge-state fields and returns the finished ari_info entry.
private function closeSegment(array $seg)
{
unset($seg['_to'], $seg['_price'],
$seg['_min_los'], $seg['_max_los'],
$seg['_min_off'], $seg['_max_off']);
return $seg;
}
// Builds a single rate object for a day.
// occupancy_based_price / occupancy_based_price_with_tax are empty objects
// (populated when the occupancy pricing module is installed).
private function buildRateObject($day)
{
return array(
'id_rate_plan' => '0',
'id_pms_rate_plan' => '0',
'price' => (float)$day['total_price_with_tax'],
'price_with_tax' => (float)$day['total_price_with_tax'],
'price_without_tax' => (float)$day['total_price_tax_excl'],
'occupancy_based_price' => new stdClass(),
'occupancy_based_price_with_tax' => new stdClass(),
'min_los_arrival' => 0,
'max_los_arrival' => 0,
'min_los_stay' => (int)$day['min_los'],
'max_los_stay' => (int)$day['max_los'],
'stop_sell' => ((int)$day['total_available_rooms'] <= 0) ? 1 : 0,
'close_to_arrival' => 0,
'close_to_departure' => 0,
'min_advanced_booking_offset' => (int)$day['minimum_booking_offset'],
'max_advanced_booking_offset' => (int)$day['maximum_booking_offset'],
);
}
// Groups a sorted list of date strings into consecutive ranges.
private function buildConsecutiveDateRanges($dates)
{
$ranges = array();
if (empty($dates)) {
return $ranges;
}
$start = $dates[0];
$prev = $dates[0];
for ($i = 1; $i < count($dates); $i++) {
$expectedNext = date('Y-m-d', strtotime($prev . ' +1 day'));
if ($dates[$i] === $expectedNext) {
$prev = $dates[$i];
} else {
$ranges[] = array('from' => $start, 'to' => $prev);
$start = $dates[$i];
$prev = $dates[$i];
}
}
$ranges[] = array('from' => $start, 'to' => $prev);
return $ranges;
}
}

View File

@@ -0,0 +1,146 @@
<?php
/**
* 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
*/
class QghcAriUpdates extends ObjectModel
{
const ARI_TYPE_AVAILABILITY = 'availability';
const ARI_TYPE_RATE = 'rate';
public $id_qghc_ari_updates;
public $id_hotel;
public $id_room_type;
public $ari_date;
public $ari_data;
public $date_add;
public $date_upd;
public static $definition = array(
'table' => 'qghc_ari_updates',
'primary' => 'id_qghc_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),
'ari_date' => array('type' => self::TYPE_DATE, 'validate' => 'isDate', 'required' => true),
'ari_data' => array('type' => self::TYPE_STRING),
'date_add' => array('type' => self::TYPE_DATE, 'validate' => 'isDate'),
'date_upd' => array('type' => self::TYPE_DATE, 'validate' => 'isDate'),
),
);
// Upsert one row per day in [dateFrom, dateTo] (inclusive).
// $changedData keys: self::ARI_TYPE_AVAILABILITY => 1 (availability changed), self::ARI_TYPE_RATE => 1 (price/LOS changed).
// On update, flags are OR-merged so a date that had both types retains both.
public function saveChangedRow($idHotel, $idRoomType, $dateFrom, $dateTo, $changedData = array())
{
if (!$idHotel || !$idRoomType) {
return false;
}
if (empty($changedData)) {
$changedData = array(self::ARI_TYPE_AVAILABILITY => 1);
}
$existing = $this->getExistingRows((int)$idHotel, (int)$idRoomType, $dateFrom, $dateTo);
for ($date = $dateFrom; $date <= $dateTo; $date = date('Y-m-d', strtotime($date . ' +1 day'))) {
if (isset($existing[$date])) {
$existingChangedData = $existing[$date]['ari_data']
? json_decode($existing[$date]['ari_data'], true)
: array();
$mergedChangedData = is_array($existingChangedData)
? array_merge($existingChangedData, $changedData)
: $changedData;
Db::getInstance()->execute(
'UPDATE `' . _DB_PREFIX_ . 'qghc_ari_updates`
SET `date_upd` = NOW(),
`ari_data` = \'' . pSQL(json_encode($mergedChangedData)) . '\'
WHERE `id_qghc_ari_updates` = ' . (int)$existing[$date]['id_qghc_ari_updates']
);
} else {
$result = Db::getInstance()->insert(
'qghc_ari_updates',
array(
'id_hotel' => (int)$idHotel,
'id_room_type' => (int)$idRoomType,
'ari_date' => $date,
'ari_data' => json_encode($changedData),
'date_add' => date('Y-m-d H:i:s'),
'date_upd' => date('Y-m-d H:i:s'),
),
false,
true,
Db::INSERT_IGNORE
);
if (!$result) {
// insert failed — silently skip; the ARI sync will catch up on next push
}
}
}
return true;
}
// Returns all changed rows for a property including ari_data.
public function getChangedRows($idHotel)
{
return Db::getInstance()->executeS(
'SELECT `id_qghc_ari_updates`, `id_hotel`, `id_room_type`, `ari_date`, `ari_data`
FROM `' . _DB_PREFIX_ . 'qghc_ari_updates`
WHERE `id_hotel` = ' . (int)$idHotel . '
ORDER BY `id_room_type` ASC, `ari_date` ASC'
);
}
// Deletes fetched rows by primary key after CM has confirmed receipt.
public function deleteChangedRowsByIds($ids)
{
if (empty($ids)) {
return;
}
$idList = implode(',', array_map('intval', $ids));
Db::getInstance()->execute(
'DELETE FROM `' . _DB_PREFIX_ . 'qghc_ari_updates`
WHERE `id_qghc_ari_updates` IN (' . $idList . ')'
);
}
private function getExistingRows($idHotel, $idRoomType, $dateFrom, $dateTo)
{
$rows = Db::getInstance()->executeS(
'SELECT `id_qghc_ari_updates`, `ari_date`, `ari_data`
FROM `' . _DB_PREFIX_ . 'qghc_ari_updates`
WHERE `id_hotel` = ' . (int)$idHotel . '
AND `id_room_type` = ' . (int)$idRoomType . '
AND `ari_date` BETWEEN \'' . pSQL($dateFrom) . '\' AND \'' . pSQL($dateTo) . '\''
);
$result = array();
if ($rows) {
foreach ($rows as $row) {
$result[date('Y-m-d', strtotime($row['ari_date']))] = $row;
}
}
return $result;
}
}

View File

@@ -0,0 +1,103 @@
<?php
/**
* 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
*/
class QghcDb
{
public function createTables()
{
$sqls = array(
// Google Hotel enrollment state lives directly on the core hotel/room-type
"ALTER TABLE `" . _DB_PREFIX_ . "htl_branch_info`
ADD `is_google_hotel_enabled` tinyint(1) NOT NULL DEFAULT 0,
ADD `id_google_hotel` varchar(255) DEFAULT NULL,
ADD `google_status` tinyint(1) NOT NULL DEFAULT 0,
ADD `failure_reason` text",
"ALTER TABLE `" . _DB_PREFIX_ . "htl_room_type`
ADD `is_google_hotel_enabled` tinyint(1) NOT NULL DEFAULT 0,
ADD `id_google_room_type` varchar(255) DEFAULT NULL",
"CREATE TABLE IF NOT EXISTS `" . _DB_PREFIX_ . "qghc_api_log` (
`id_qghc_api_log` int(11) NOT NULL AUTO_INCREMENT,
`id_hotel` int(11) NOT NULL,
`api_type` varchar(50) NOT NULL,
`request` longtext,
`response` longtext,
`status` tinyint(1) NOT NULL DEFAULT 0,
`message` text,
`date_add` datetime NOT NULL,
PRIMARY KEY (`id_qghc_api_log`),
KEY `id_hotel` (`id_hotel`),
KEY `api_type` (`api_type`),
KEY `date_add` (`date_add`)
) ENGINE=" . _MYSQL_ENGINE_ . " DEFAULT CHARSET=utf8mb4",
"CREATE TABLE IF NOT EXISTS `" . _DB_PREFIX_ . "qghc_ari_updates` (
`id_qghc_ari_updates` int(11) NOT NULL AUTO_INCREMENT,
`id_hotel` int(11) NOT NULL,
`id_room_type` int(11) NOT NULL,
`ari_date` date NOT NULL,
`ari_data` longtext NULL,
`date_add` datetime NOT NULL,
`date_upd` datetime NOT NULL,
PRIMARY KEY (`id_qghc_ari_updates`),
UNIQUE KEY `hotel_room_date` (`id_hotel`, `id_room_type`, `ari_date`),
KEY `id_hotel` (`id_hotel`)
) ENGINE=" . _MYSQL_ENGINE_ . " DEFAULT CHARSET=utf8mb4",
);
foreach ($sqls as $sql) {
if (!Db::getInstance()->execute(trim($sql))) {
return false;
}
}
return true;
}
public function dropTables()
{
$sqls = array(
"DROP TABLE IF EXISTS `" . _DB_PREFIX_ . "qghc_ari_updates`",
"DROP TABLE IF EXISTS `" . _DB_PREFIX_ . "qghc_api_log`",
"ALTER TABLE `" . _DB_PREFIX_ . "htl_room_type`
DROP COLUMN `is_google_hotel_enabled`,
DROP COLUMN `id_google_room_type`",
"ALTER TABLE `" . _DB_PREFIX_ . "htl_branch_info`
DROP COLUMN `is_google_hotel_enabled`,
DROP COLUMN `id_google_hotel`,
DROP COLUMN `google_status`,
DROP COLUMN `failure_reason`",
);
foreach ($sqls as $sql) {
if (!Db::getInstance()->execute(trim($sql))) {
return false;
}
}
return true;
}
}

View File

@@ -0,0 +1,214 @@
<?php
/**
* 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
*/
class QghcProperty
{
const GOOGLE_STATUS_NOT_CONNECTED = -1;
const GOOGLE_STATUS_PENDING = 0;
const GOOGLE_STATUS_CONNECTED = 1;
const GOOGLE_STATUS_FAILED = 2;
/** @var int Set to the hotel's own id — there's no separate property row anymore. */
public $id;
public $id_hotel;
public $id_google_hotel;
public $google_status;
public $failure_reason;
private function __construct($idHotel, array $row)
{
$this->id = (int) $idHotel;
$this->id_hotel = (int) $idHotel;
$this->id_google_hotel = $row['id_google_hotel'];
$this->google_status = (int) $row['google_status'];
$this->failure_reason = $row['failure_reason'];
}
/**
* Returns the QghcProperty instance for a hotel, or null if not enrolled.
*
* @param int $idHotel
* @return QghcProperty|null
*/
public static function getByHotelId($idHotel)
{
$row = Db::getInstance()->getRow(
'SELECT `is_google_hotel_enabled`, `id_google_hotel`, `google_status`, `failure_reason`
FROM `' . _DB_PREFIX_ . 'htl_branch_info`
WHERE `id` = ' . (int) $idHotel
);
if (!$row || !$row['is_google_hotel_enabled']) {
return null;
}
return new self($idHotel, $row);
}
/**
* Enrolls a hotel and returns the instance. google_status resets to 0 (pending)
* until the Channel Manager assigns a Google ID.
*
* @param int $idHotel
* @return QghcProperty|null null on DB failure
*/
public static function createForHotel($idHotel)
{
$updated = Db::getInstance()->update(
'htl_branch_info',
array(
'is_google_hotel_enabled' => 1,
'google_status' => self::GOOGLE_STATUS_PENDING,
),
'`id` = ' . (int) $idHotel
);
if (!$updated) {
return null;
}
return new self($idHotel, array('id_google_hotel' => null, 'google_status' => self::GOOGLE_STATUS_PENDING, 'failure_reason' => null));
}
/**
* Stores the Google-assigned hotel ID, connection status, and (when FAILED) the
* rejection reason(s) received from the Channel Manager.
*
* @param string $idGoogleHotel
* @param int $googleStatus
* @param string|null $failureReason
* @return bool
*/
public function applyGoogleStatus($idGoogleHotel, $googleStatus, $failureReason = null)
{
$this->id_google_hotel = (string) $idGoogleHotel;
$this->google_status = (int) $googleStatus;
$this->failure_reason = $failureReason;
return Db::getInstance()->update(
'htl_branch_info',
array(
'id_google_hotel' => pSQL($this->id_google_hotel),
'google_status' => $this->google_status,
'failure_reason' => $this->failure_reason !== null ? pSQL($this->failure_reason) : null,
),
'`id` = ' . (int) $this->id_hotel
);
}
/**
* Un-enrolls this hotel (and, via cascade, all its room type mappings) — resets
* the flag columns to their disabled defaults. Never touches the hotel row itself
* beyond these columns.
*
* @return bool
*/
public function delete()
{
QghcRoomType::deleteByPropertyId((int) $this->id_hotel);
return Db::getInstance()->update(
'htl_branch_info',
array(
'is_google_hotel_enabled' => 0,
'id_google_hotel' => null,
'google_status' => 0,
'failure_reason' => null,
),
'`id` = ' . (int) $this->id_hotel
);
}
/**
* Returns the number of hotels currently enrolled in Google Hotel (any status).
*
* @return int
*/
public static function countHotelsLinkedToGoogle()
{
return (int) Db::getInstance()->getValue(
'SELECT COUNT(*) FROM `' . _DB_PREFIX_ . 'htl_branch_info`
WHERE `is_google_hotel_enabled` = 1'
);
}
/**
* Returns all enrolled properties with their hotel name for the config page.
*
* @param int $idLang
* @return array|false
*/
public static function getEnrolledProperties($idLang)
{
return Db::getInstance()->executeS(
'SELECT hbi.`id` AS `id_hotel`, hbi.`google_status`, hbi.`date_upd` AS `date_add`,
COALESCE(hbl.`hotel_name`, \'\') AS `hotel_name`
FROM `' . _DB_PREFIX_ . 'htl_branch_info` hbi
LEFT JOIN `' . _DB_PREFIX_ . 'htl_branch_info_lang` hbl
ON hbl.`id` = hbi.`id` AND hbl.`id_lang` = ' . (int) $idLang . '
WHERE hbi.`is_google_hotel_enabled` = 1
ORDER BY hbl.`hotel_name` ASC'
);
}
/**
* Returns every hotel with its name, for the API Logs page's Hotel filter dropdown.
*
* @param int $idLang
* @return array|false
*/
public static function getAllHotels($idLang)
{
return Db::getInstance()->executeS(
'SELECT hbi.`id`, COALESCE(hbl.`hotel_name`, \'\') AS `hotel_name`
FROM `' . _DB_PREFIX_ . 'htl_branch_info` hbi
LEFT JOIN `' . _DB_PREFIX_ . 'htl_branch_info_lang` hbl
ON hbl.`id` = hbi.`id` AND hbl.`id_lang` = ' . (int)$idLang . '
ORDER BY hbl.`hotel_name` ASC'
);
}
/**
* Returns the SELECT fragment added to the hotel listing query. The enrollment
* columns now live on the same row being listed (alias `a`) — no join needed.
* Used by hookActionAdminAddHotelListingFieldsModifier.
*
* @return string
*/
public static function getHotelListingSelect()
{
return ', IF(a.`is_google_hotel_enabled` = 1, a.`google_status`, '
. self::GOOGLE_STATUS_NOT_CONNECTED . ') AS `google_status`';
}
/**
* No join needed anymore — kept as a no-op so the one caller (which appends this
* to a query string) doesn't need to change.
*
* @return string
*/
public static function getHotelListingJoin()
{
return '';
}
}

View File

@@ -0,0 +1,806 @@
<?php
/**
* 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
*/
if (!defined('_PS_VERSION_')) {
exit;
}
/**
* Business logic for enabling/disabling hotels on Google Hotel Connector.
* Enrollment state lives on columns bolted onto htl_branch_info/htl_room_type — all
* CRUD on those columns is delegated to QghcProperty and QghcRoomType. Only
* cross-domain queries (joining into address, product_lang, etc.) live here.
*/
class QghcPropertyService
{
private static function l($string)
{
return Translate::getModuleTranslation('qlogooglehotelconnector', $string, 'QghcPropertyService');
}
/**
* Total price for a room/date range, pinned to the shop's default currency
* regardless of the visitor's session — keeps the ARI feed and JSON-LD from
* ever diverging on currency.
*
* @param int $idProduct
* @param string $dateFrom
* @param string $dateTo
* @param int $qty
* @return array Same shape as HotelRoomTypeFeaturePricing::getRoomTypeTotalPrice().
*/
public static function getRoomTypeTotalPriceInDefaultCurrency($idProduct, $dateFrom, $dateTo, $qty = 1)
{
$context = Context::getContext();
$sessionCurrency = $context->currency;
$context->currency = new Currency((int)Configuration::get('PS_CURRENCY_DEFAULT'));
$priceData = HotelRoomTypeFeaturePricing::getRoomTypeTotalPrice($idProduct, $dateFrom, $dateTo, $qty);
$context->currency = $sessionCurrency;
return $priceData;
}
/**
* Final total price for a room/date range — the room price (tax incl/excl, from
* getRoomTypeTotalPriceInDefaultCurrency() above) PLUS any Convenience-Fee-type
* auto-add services (tax incl/excl). Deliberately separate from that method: it
* returns the bare room rate and is also used by the ARI feed (QghcAri.php), which
* must NOT include Convenience Fee. This method exists only for Google Hotel SDM
* (QghcSdmService), which has no cart to read a real checkout total from, so it
* recomputes the same two core-pricing calls Cart::getSummaryDetails() itself
* relies on for "Total rooms cost" + "Convenience Fees" — see
* HotelRoomTypeFeaturePricing::getRoomTypeTotalPrice() (via the method above) and
* RoomTypeServiceProduct::getAutoAddServices(..., PRICE_ADDITION_TYPE_INDEPENDENT).
* Cart rules/discounts and customer-selected extra demands are intentionally
* excluded — those are session/customer-specific, not a property of the room type
* itself, so they don't belong in a generic structured-data price.
*
* @param int $idProduct
* @param string $dateFrom
* @param string $dateTo
* @param int $qty
* @return array Same shape as getRoomTypeTotalPriceInDefaultCurrency(), with any
* Convenience Fee already added into both tax-incl and tax-excl totals.
*/
public static function getRoomTypeFinalPriceForSdm($idProduct, $dateFrom, $dateTo, $qty = 1)
{
$priceData = self::getRoomTypeTotalPriceInDefaultCurrency($idProduct, $dateFrom, $dateTo, $qty);
$context = Context::getContext();
$sessionCurrency = $context->currency;
$context->currency = new Currency((int)Configuration::get('PS_CURRENCY_DEFAULT'));
$feesInclTax = RoomTypeServiceProduct::getAutoAddServices($idProduct, $dateFrom, $dateTo, Product::PRICE_ADDITION_TYPE_INDEPENDENT, true, 1);
$feesExclTax = RoomTypeServiceProduct::getAutoAddServices($idProduct, $dateFrom, $dateTo, Product::PRICE_ADDITION_TYPE_INDEPENDENT, false, 1);
$context->currency = $sessionCurrency;
if ($feesInclTax) {
foreach ($feesInclTax as $fee) {
$priceData['total_price_tax_incl'] += (float) $fee['price'];
}
}
if ($feesExclTax) {
foreach ($feesExclTax as $fee) {
$priceData['total_price_tax_excl'] += (float) $fee['price'];
}
}
return $priceData;
}
/**
* ISO code of the shop's default currency.
*
* @return string
*/
public static function getDefaultCurrencyIsoCode()
{
static $isoCode = null;
if (null === $isoCode) {
$isoCode = (new Currency((int)Configuration::get('PS_CURRENCY_DEFAULT')))->iso_code;
}
return $isoCode;
}
/**
* Validates required Google Hotel fields for a hotel.
* Reads coordinates directly from DB — ObjectModel cache may hold stale 0 values
* from an update() call that ran earlier in the same request.
*
* @param int $idHotel
* @param int $idLang
* @return string[] Error messages; empty = all fields valid.
*/
public static function validateRequiredFields($idHotel, $idLang)
{
$hotelAddress = HotelBranchInformation::getAddress((int)$idHotel, (int)$idLang);
$errors = array();
if (empty($hotelAddress['address1'])) {
$errors[] = self::l('Google Hotel: Street address is required before enabling Google Hotel.');
}
if (empty($hotelAddress['city'])) {
$errors[] = self::l('Google Hotel: City is required before enabling Google Hotel.');
}
if (empty($hotelAddress['id_country'])) {
$errors[] = self::l('Google Hotel: Country is required before enabling Google Hotel.');
}
if (!empty($hotelAddress['id_country']) && Country::containsStates((int)$hotelAddress['id_country']) && empty($hotelAddress['id_state'])) {
$errors[] = self::l('Google Hotel: State / Region is required before enabling Google Hotel.');
}
if (!self::hasValidCoordinates($idHotel)) {
$errors[] = self::l('Google Hotel: Latitude and longitude are required before enabling Google Hotel.');
}
return $errors;
}
/**
* Reasons a hotel isn't listed on Google Hotel — empty means it's fine (connected, or
* enrolled and legitimately PENDING Google's routine weekly review, which is not an
* issue).
*
* @param int $idHotel
* @param int $idLang
* @return string[]
*/
public static function getHotelConnectionIssueReasons($idHotel, $idLang)
{
$property = QghcProperty::getByHotelId($idHotel);
if ($property) {
if ((int) $property->google_status === QghcProperty::GOOGLE_STATUS_FAILED) {
return array($property->failure_reason
? self::l('Google Hotel: Google could not verify this hotel — ') . $property->failure_reason
: self::l('Google Hotel: Google reported an issue connecting this hotel. Please contact support.'));
}
return array();
}
$errors = self::validateRequiredFields($idHotel, $idLang);
if ($errors) {
return $errors;
}
return array(self::l('Google Hotel: This hotel is not yet enabled for Google Hotel.'));
}
/**
* Hotels not currently listed on Google Hotel, for any reason, except one legitimately
* PENDING Google's routine review. Re-validates live on every call — no schema to
* persist this, and unnecessary at typical hotel-chain PMS hotel counts.
*
* @param int $idLang
* @return array id_hotel => hotel_name
*/
public static function getHotelsWithIssues($idLang)
{
$issues = array();
foreach ((array) QghcProperty::getAllHotels($idLang) as $hotel) {
$idHotel = (int) $hotel['id'];
if (self::getHotelConnectionIssueReasons($idHotel, $idLang)) {
$issues[$idHotel] = $hotel['hotel_name'];
}
}
return $issues;
}
/**
* Checks whether a hotel has valid coordinates set.
* Validates with Validate::isFloat() to match core's own check when saving a hotel
* (see AdminAddHotelController::processSave(), which validates latitude/longitude
* the same way before storing them) — a non-numeric value is treated the same as unset.
* Only (0.0, 0.0) together counts as "unset" — latitude 0.0 (the Equator) and
* longitude 0.0 (the Prime Meridian) are each real, valid coordinates on their own,
* so one of them being 0.0 while the other has a real value is a legitimate location.
* Reads directly from DB — ObjectModel cache may hold stale 0 values from an update()
* call that ran earlier in the same request.
*
* @param int $idHotel
* @return bool
*/
private static function hasValidCoordinates($idHotel)
{
$coords = Db::getInstance()->getRow(
'SELECT `latitude`, `longitude` FROM `' . _DB_PREFIX_ . 'htl_branch_info`
WHERE `id` = ' . (int)$idHotel
);
if (!$coords || !Validate::isFloat($coords['latitude']) || !Validate::isFloat($coords['longitude'])) {
return false;
}
return (float)$coords['latitude'] != 0.0 || (float)$coords['longitude'] != 0.0;
}
/**
* Disables QGHC for a hotel.
* QghcProperty::delete() cascades to remove all room type mappings automatically.
*
* @param int $idHotel
*/
public static function disable($idHotel)
{
$property = QghcProperty::getByHotelId($idHotel);
if (!$property) {
return;
}
try {
Db::getInstance()->execute('START TRANSACTION');
$property->delete();
Db::getInstance()->execute('COMMIT');
} catch (Exception $e) {
Db::getInstance()->execute('ROLLBACK');
return;
}
}
/**
* Enables QGHC for a hotel and syncs the given room type IDs.
* Creates the property row if it does not yet exist. Room types not in
* $selectedRoomTypes are un-enrolled (and their Google ID cleared); room types
* already enrolled that stay in $selectedRoomTypes are left untouched, keeping
* whatever Google ID the CM already assigned them — only actually-deselected room
* types ever lose their Google ID.
*
* @param int $idHotel
* @param int[] $selectedRoomTypes htl_room_type.id_product values to connect.
*/
public static function enable($idHotel, array $selectedRoomTypes)
{
$property = QghcProperty::getByHotelId($idHotel);
if (!$property) {
$property = QghcProperty::createForHotel($idHotel);
if (!$property) {
return;
}
}
$idQghcProperty = (int)$property->id;
try {
Db::getInstance()->execute('START TRANSACTION');
QghcRoomType::deleteExcept($idQghcProperty, $selectedRoomTypes);
if (!empty($selectedRoomTypes)) {
$idList = implode(',', array_map('intval', $selectedRoomTypes));
$roomRows = Db::getInstance()->executeS(
'SELECT `id_product` AS id_room_type FROM `' . _DB_PREFIX_ . 'htl_room_type`
WHERE `id_product` IN (' . $idList . ')'
);
if ($roomRows) {
foreach ($roomRows as $room) {
QghcRoomType::insertForProperty($idQghcProperty, (int)$room['id_room_type']);
}
}
}
Db::getInstance()->execute('COMMIT');
} catch (Exception $e) {
Db::getInstance()->execute('ROLLBACK');
}
}
/**
* Loads all data required for the Google Hotel tab in the hotel edit form.
*
* @param int $idHotel
* @param int $idLang
* @return array Keys: ghcProperty, roomTypes, roomTypeMappings, missingFields
*/
public static function getHotelTabData($idHotel, $idLang)
{
$data = array(
'ghcProperty' => array(),
'roomTypes' => array(),
'roomTypeMappings' => array(),
'missingFields' => array(),
);
if (!$idHotel) {
return $data;
}
$property = QghcProperty::getByHotelId($idHotel);
if ($property) {
$data['ghcProperty'] = array(
'id' => (int)$property->id,
'id_hotel' => (int)$property->id_hotel,
'google_status'=> (int)$property->google_status,
);
foreach (QghcRoomType::getMappingsForHotel($idHotel) as $idRoomType) {
$data['roomTypeMappings'][$idRoomType] = true;
}
}
$objRoomType = new HotelRoomType();
$data['roomTypes'] = (array) $objRoomType->getRoomTypeByHotelId((int)$idHotel, (int)$idLang);
/* Collect missing required fields for inline UI display. */
$hotelAddress = HotelBranchInformation::getAddress((int)$idHotel, (int)$idLang);
if (empty($hotelAddress['address1'])) {
$data['missingFields'][] = self::l('Street address');
}
if (empty($hotelAddress['city'])) {
$data['missingFields'][] = self::l('City');
}
if (empty($hotelAddress['id_country'])) {
$data['missingFields'][] = self::l('Country');
}
if (!empty($hotelAddress['id_country']) && Country::containsStates((int)$hotelAddress['id_country']) && empty($hotelAddress['id_state'])) {
$data['missingFields'][] = self::l('State / Region');
}
if (!self::hasValidCoordinates($idHotel)) {
$data['missingFields'][] = self::l('Latitude and Longitude');
}
return $data;
}
/**
* Returns the Google-assigned hotel ID for a property, or empty string if not set.
*
* @param int $idHotel
* @return string
*/
public static function getGoogleHotelId($idHotel)
{
$property = QghcProperty::getByHotelId($idHotel);
if (!$property || !$property->id_google_hotel) {
return '';
}
return (string)$property->id_google_hotel;
}
/**
* Maps the Channel Manager's own `google_status` values (0=NOT_SENT, 1=ACTIVE,
* 2=PENDING, 3=FAILED) onto QghcProperty's internal constants. These two numbering
* schemes are NOT the same (CM's ACTIVE=1 happens to match our CONNECTED=1, but
* CM's PENDING=2 and FAILED=3 do not match our PENDING=0/FAILED=2) — never store or
* compare the CM's raw value directly against QghcProperty::GOOGLE_STATUS_* elsewhere.
*
* @param int $cmStatus
* @return int One of QghcProperty::GOOGLE_STATUS_*
*/
private static function mapGoogleStatus($cmStatus)
{
switch ((int) $cmStatus) {
case 1: // ACTIVE
return QghcProperty::GOOGLE_STATUS_CONNECTED;
case 3: // FAILED
return QghcProperty::GOOGLE_STATUS_FAILED;
case 0: // NOT_SENT
case 2: // PENDING
default:
return QghcProperty::GOOGLE_STATUS_PENDING;
}
}
/**
* Applies the property/room-type Google IDs and status pushed by the Channel Manager's
* `google_status` webservice callback.
*
* @param array $values Keys: google_status (0-3, see mapGoogleStatus()), failure_reason,
* properties (id_hotel => id_google_hotel), room_types (id_product => id_google_room_type), send_mail
* @return array Keys: google_status, updated_properties (int[]), updated_room_types (int[])
*/
public static function applyGoogleStatusUpdate(array $values)
{
$googleStatus = self::mapGoogleStatus(isset($values['google_status']) ? $values['google_status'] : 0);
$failureReason = ($googleStatus === QghcProperty::GOOGLE_STATUS_FAILED && !empty($values['failure_reason']))
? (string) $values['failure_reason']
: null;
$properties = (isset($values['properties']) && is_array($values['properties'])) ? $values['properties'] : array();
$roomTypes = (isset($values['room_types']) && is_array($values['room_types'])) ? $values['room_types'] : array();
$sendMail = !empty($values['send_mail']);
$updatedProperties = array();
foreach ($properties as $idHotel => $idGoogleHotel) {
$idHotel = (int)$idHotel;
if (!$idHotel) {
continue;
}
$property = QghcProperty::getByHotelId($idHotel);
if (!$property) {
continue;
}
$property->applyGoogleStatus($idGoogleHotel, $googleStatus, $failureReason);
$updatedProperties[] = $idHotel;
if ($sendMail) {
if ($googleStatus === QghcProperty::GOOGLE_STATUS_CONNECTED) {
self::sendGoogleHotelLiveEmail($idHotel);
} elseif ($googleStatus === QghcProperty::GOOGLE_STATUS_FAILED) {
self::sendGoogleHotelFailedEmail($idHotel, $failureReason);
}
}
}
$updatedRoomTypes = array();
foreach ($roomTypes as $idProduct => $idGoogleRoomType) {
$idProduct = (int)$idProduct;
if (!$idProduct) {
continue;
}
$roomType = QghcRoomType::getByRoomTypeId($idProduct);
if (!$roomType) {
continue;
}
$roomType->applyGoogleRoomType($idGoogleRoomType);
$updatedRoomTypes[] = $idProduct;
}
return array(
'google_status' => $googleStatus,
'updated_properties' => $updatedProperties,
'updated_room_types' => $updatedRoomTypes,
);
}
/**
* Emails the shop owner that a hotel is now live on Google Hotel Center.
* Best-effort — Mail::Send() failures must never fail the webservice response
* that triggered this (see applyGoogleStatusUpdate()).
*
* @param int $idHotel
*/
private static function sendGoogleHotelLiveEmail($idHotel)
{
$shopEmail = (string) Configuration::get('PS_SHOP_EMAIL');
if (!Validate::isEmail($shopEmail)) {
return;
}
$idLang = (int) Configuration::get('PS_LANG_DEFAULT');
$idShop = (int) Configuration::get('PS_SHOP_DEFAULT');
$idCategory = (int) Db::getInstance()->getValue(
'SELECT `id_category` FROM `' . _DB_PREFIX_ . 'htl_branch_info` WHERE `id` = ' . (int) $idHotel
);
$hotelUrl = $idCategory
? Context::getContext()->link->getCategoryLink(
new Category($idCategory, $idLang, $idShop),
null,
$idLang,
null,
$idShop
)
: '';
Mail::Send(
$idLang,
'google_hotel_live',
Mail::l('Your hotel is now live on Google Hotel Center', $idLang),
array(
'{hotel_name}' => self::getHotelName($idHotel, $idLang),
'{hotel_url}' => $hotelUrl,
),
$shopEmail,
(string) Configuration::get('PS_SHOP_NAME'),
null,
null,
null,
null,
_PS_MODULE_DIR_ . 'qlogooglehotelconnector/mails/'
);
}
/**
* Emails the shop owner that a hotel FAILED Google Hotel Center verification.
* Best-effort — Mail::Send() failures must never fail the webservice response
* that triggered this (see applyGoogleStatusUpdate()).
*
* @param int $idHotel
* @param string|null $failureReason
*/
private static function sendGoogleHotelFailedEmail($idHotel, $failureReason)
{
$shopEmail = (string) Configuration::get('PS_SHOP_EMAIL');
if (!Validate::isEmail($shopEmail)) {
return;
}
$idLang = (int) Configuration::get('PS_LANG_DEFAULT');
Mail::Send(
$idLang,
'google_hotel_failed',
Mail::l('Google Hotel Center could not verify your hotel', $idLang),
array(
'{hotel_name}' => self::getHotelName($idHotel, $idLang),
'{failure_reason}' => $failureReason ? $failureReason : self::l('No specific reason was provided.'),
),
$shopEmail,
(string) Configuration::get('PS_SHOP_NAME'),
null,
null,
null,
null,
_PS_MODULE_DIR_ . 'qlogooglehotelconnector/mails/'
);
}
/**
* Returns the hotel display name for the given language.
*
* @param int $idHotel
* @param int $idLang
* @return string
*/
public static function getHotelName($idHotel, $idLang)
{
return (string)Db::getInstance()->getValue(
'SELECT `hotel_name` FROM `' . _DB_PREFIX_ . 'htl_branch_info_lang`
WHERE `id` = ' . (int)$idHotel . ' AND `id_lang` = ' . (int)$idLang
);
}
/**
* Returns only the room types actually enrolled in Google Hotel for this hotel.
*
* @param int $idHotel
* @param int $idLang
* @return array
*/
public static function getHotelRoomTypesForSdm($idHotel, $idLang)
{
return Db::getInstance()->executeS(
'SELECT hrt.`id_product`, pl.`name` AS room_name,
hrt.`id_google_room_type`,
hrt.`max_adults`, hrt.`max_children`
FROM `' . _DB_PREFIX_ . 'htl_room_type` hrt
INNER JOIN `' . _DB_PREFIX_ . 'product_lang` pl
ON pl.`id_product` = hrt.`id_product`
AND pl.`id_lang` = ' . (int)$idLang . '
WHERE hrt.`id_hotel` = ' . (int)$idHotel . '
AND hrt.`is_google_hotel_enabled` = 1'
);
}
/**
* Returns the hotel ID for a room type product, or null if not found.
* Used by ARI change-tracking hooks in the main module class.
*
* @param int $idProduct
* @return int|null
*/
public static function getHotelIdForProduct($idProduct)
{
$objHotelRoomType = new HotelRoomType();
$roomTypeInfo = $objHotelRoomType->getRoomTypeInfoByIdProduct($idProduct);
return $roomTypeInfo ? (int)$roomTypeInfo['id_hotel'] : null;
}
/**
* Builds the Google Hotel property + room type feed for the `google-hotel-properties` webservice endpoint.
* Cross-domain query (htl_branch_info, address, htl_room_type, product_lang) — kept as raw SQL.
*
* @return string JSON-encoded feed
*/
public static function getPropertiesFeed()
{
$idLang = (int) Configuration::get('PS_LANG_DEFAULT');
$idShop = (int) Configuration::get('PS_SHOP_DEFAULT');
$rows = Db::getInstance(_PS_USE_SQL_SLAVE_)->executeS(
'SELECT
hbi.`id` AS id_pms_property,
hbl.`hotel_name`,
COALESCE(hbl.`description`, \'\') AS `description`,
COALESCE(hbi.`email`, \'\') AS `email`,
hbi.`latitude`,
hbi.`longitude`,
hbi.`id_category`,
COALESCE(a.`address1`, \'\') AS `address1`,
COALESCE(a.`address2`, \'\') AS `address2`,
COALESCE(a.`city`, \'\') AS `city`,
COALESCE(a.`postcode`, \'\') AS `zip_code`,
COALESCE(a.`phone`, \'\') AS `phone`,
COALESCE(c.`iso_code`, \'\') AS `country_code`,
COALESCE(c.`call_prefix`, \'\') AS `dial_code`,
COALESCE(s.`name`, \'\') AS `state`,
hrt.`id_product` AS id_pms_room_type,
COALESCE(pl.`name`, \'\') AS room_type_name,
hrt.`max_adults`,
hrt.`max_children`,
hrt.`adults` AS base_occupancy,
(
SELECT COUNT(*)
FROM `' . _DB_PREFIX_ . 'htl_room_information` hri
WHERE hri.`id_product` = hrt.`id_product`
AND hri.`id_hotel` = hbi.`id`
AND hri.`id_status` != ' . (int) HotelRoomInformation::STATUS_INACTIVE . '
) AS total_rooms
FROM `' . _DB_PREFIX_ . 'htl_branch_info` hbi
LEFT JOIN `' . _DB_PREFIX_ . 'htl_branch_info_lang` hbl
ON hbl.`id` = hbi.`id` AND hbl.`id_lang` = ' . $idLang . '
LEFT JOIN `' . _DB_PREFIX_ . 'address` a
ON a.`id_hotel` = hbi.`id` AND a.`deleted` = 0
LEFT JOIN `' . _DB_PREFIX_ . 'country` c
ON c.`id_country` = a.`id_country`
LEFT JOIN `' . _DB_PREFIX_ . 'state` s
ON s.`id_state` = a.`id_state`
INNER JOIN `' . _DB_PREFIX_ . 'htl_room_type` hrt
ON hrt.`id_hotel` = hbi.`id`
AND hrt.`is_google_hotel_enabled` = 1
LEFT JOIN `' . _DB_PREFIX_ . 'product_lang` pl
ON pl.`id_product` = hrt.`id_product`
AND pl.`id_lang` = ' . $idLang . '
AND pl.`id_shop` = ' . $idShop . '
WHERE hbi.`is_google_hotel_enabled` = 1
ORDER BY hbi.`id` ASC, hrt.`id_product` ASC'
);
if (!$rows) {
return json_encode(array('success' => true, 'data' => array()));
}
$currency = Db::getInstance(_PS_USE_SQL_SLAVE_)->getRow(
'SELECT `iso_code` FROM `' . _DB_PREFIX_ . 'currency`
WHERE `id_currency` = ' . (int) Configuration::get('PS_CURRENCY_DEFAULT')
);
$currencyCode = $currency ? $currency['iso_code'] : '';
$timezone = Configuration::get('PS_TIMEZONE');
if (!$timezone) {
$timezone = @date_default_timezone_get();
}
// Global setting (not per-hotel) — tells the CM whether hotel_url below is a
// pretty rewritten slug or a plain index.php?...&id_category=... query string.
$isFriendlyUrl = (bool) Configuration::get('PS_REWRITING_SETTINGS');
$hotels = array();
foreach ($rows as $row) {
$idHotel = (int) $row['id_pms_property'];
if (!isset($hotels[$idHotel])) {
$idCategory = (int) $row['id_category'];
$hotelUrl = $idCategory
? Context::getContext()->link->getCategoryLink(
new Category($idCategory, $idLang, $idShop),
null,
$idLang,
null,
$idShop
)
: '';
$hotels[$idHotel] = array(
'id' => (string) $idHotel,
'name' => $row['hotel_name'],
'property_type' => 'hotel',
'description' => trim(strip_tags($row['description'])),
'email' => $row['email'],
'phone' => $row['phone'],
'dial_code' => $row['dial_code'],
'currency' => $currencyCode,
'country_code' => $row['country_code'],
'state' => $row['state'],
'city' => $row['city'],
'address1' => $row['address1'],
'address2' => $row['address2'],
'zip_code' => $row['zip_code'],
'latitude' => $row['latitude'],
'longitude' => $row['longitude'],
'timezone' => $timezone,
'currency_code' => $currencyCode,
'is_friendly_url' => $isFriendlyUrl,
'hotel_url' => $hotelUrl,
'room_types' => array(),
);
}
$hotels[$idHotel]['room_types'][] = array(
'id' => (string) $row['id_pms_room_type'],
'id_property' => (string) $idHotel,
'name' => $row['room_type_name'],
'total_rooms' => (int) $row['total_rooms'],
'base_occupancy' => (int) $row['base_occupancy'],
'max_adults' => (int) $row['max_adults'],
'max_children' => (int) $row['max_children'],
'max_infants' => 0,
);
}
return json_encode(array('success' => true, 'data' => array_values($hotels)));
}
/**
* Processes the GHC enable/disable form submission after a hotel is saved.
* Uses a static guard so multiple hooks firing in the same request only run once.
*
* @param int $idHotel
*/
public static function processDataSave($idHotel)
{
static $dataSaved = false;
if ($dataSaved) {
return;
}
$hotelStatus = Tools::getValue('hotel_status');
if (false === $hotelStatus || !$idHotel) {
return;
}
$dataSaved = true;
if (0 === (int) $hotelStatus) {
self::disable($idHotel);
return;
}
$idLang = (int) Context::getContext()->language->id;
$fieldErrors = self::validateRequiredFields($idHotel, $idLang);
if (!empty($fieldErrors)) {
return;
}
$selectedRoomTypes = Tools::getValue('room_types', array());
$selectedRoomTypes = is_array($selectedRoomTypes)
? array_unique(array_map('intval', $selectedRoomTypes))
: array();
if (empty($selectedRoomTypes)) {
return;
}
self::enable($idHotel, $selectedRoomTypes);
}
}

View File

@@ -0,0 +1,34 @@
<?php
/**
* 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
*/
include_once "QghcDb.php";
include_once "QghcApiLog.php";
include_once "QghcAriUpdates.php";
include_once "QghcProperty.php";
include_once "QghcRoomType.php";
include_once "QghcAri.php";
include_once "QghcApiService.php";
include_once "WebserviceSpecificManagementGhcApi.php";
include_once "QghcPropertyService.php";
include_once "QghcSdmService.php";
include_once "QghcWebserviceSetup.php";

View File

@@ -0,0 +1,156 @@
<?php
/**
* 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 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
*/
class QghcRoomType
{
public $id_room_type;
public $id_google_room_type;
/**
* Returns the QghcRoomType instance for a PMS room type product ID, or null if not enrolled.
*
* @param int $idRoomType htl_room_type.id_product
* @return QghcRoomType|null
*/
public static function getByRoomTypeId($idRoomType)
{
$row = Db::getInstance()->getRow(
'SELECT `is_google_hotel_enabled`, `id_google_room_type` FROM `' . _DB_PREFIX_ . 'htl_room_type`
WHERE `id_product` = ' . (int) $idRoomType
);
if (!$row || !$row['is_google_hotel_enabled']) {
return null;
}
$obj = new self();
$obj->id_room_type = (int) $idRoomType;
$obj->id_google_room_type = $row['id_google_room_type'];
return $obj;
}
/**
* Enrolls one room type under a hotel.
*
* @param int $idHotel
* @param int $idRoomType htl_room_type.id_product
* @return bool
*/
public static function insertForProperty($idHotel, $idRoomType)
{
return Db::getInstance()->update(
'htl_room_type',
array('is_google_hotel_enabled' => 1),
'`id_product` = ' . (int) $idRoomType . ' AND `id_hotel` = ' . (int) $idHotel
);
}
/**
* Stores the Google-assigned room type ID received from the Channel Manager.
*
* @param string $idGoogleRoomType
* @return bool
*/
public function applyGoogleRoomType($idGoogleRoomType)
{
$this->id_google_room_type = (string) $idGoogleRoomType;
return Db::getInstance()->update(
'htl_room_type',
array('id_google_room_type' => pSQL($this->id_google_room_type)),
'`id_product` = ' . (int) $this->id_room_type
);
}
/**
* Un-enrolls all room types for a hotel.
*
* @param int $idHotel
* @return bool
*/
public static function deleteByPropertyId($idHotel)
{
return Db::getInstance()->update(
'htl_room_type',
array('is_google_hotel_enabled' => 0, 'id_google_room_type' => null),
'`id_hotel` = ' . (int) $idHotel
);
}
/**
* Un-enrolls only the room types of a hotel NOT in $keepRoomTypeIds, clearing their
* Google ID. Room types already enrolled and staying in $keepRoomTypeIds are left
* completely untouched — including whatever Google ID the CM already assigned them.
* Used by QghcPropertyService::enable() so re-selecting a hotel's room types never
* wipes the Google ID of a room type that was never actually deselected.
*
* @param int $idHotel
* @param int[] $keepRoomTypeIds htl_room_type.id_product values to leave untouched.
* @return bool
*/
public static function deleteExcept($idHotel, array $keepRoomTypeIds)
{
$keepIds = array_map('intval', $keepRoomTypeIds);
$keepList = $keepIds ? implode(',', $keepIds) : '0';
return Db::getInstance()->update(
'htl_room_type',
array('is_google_hotel_enabled' => 0, 'id_google_room_type' => null),
'`id_hotel` = ' . (int) $idHotel . ' AND `id_product` NOT IN (' . $keepList . ')'
);
}
/**
* Returns id_product values of all room types enrolled for a hotel.
* Used by QghcAri to restrict ARI output to only connected room types.
*
* @param int $idHotel
* @return int[]
*/
public static function getConnectedProductIds($idHotel)
{
$rows = Db::getInstance()->executeS(
'SELECT `id_product` FROM `' . _DB_PREFIX_ . 'htl_room_type`
WHERE `id_hotel` = ' . (int) $idHotel . ' AND `is_google_hotel_enabled` = 1'
);
$ids = array();
if ($rows) {
foreach ($rows as $row) {
$ids[] = (int) $row['id_product'];
}
}
return $ids;
}
/**
* Returns id_product values enrolled for a hotel (for the hotel-edit tab checkbox state).
*
* @param int $idHotel
* @return int[]
*/
public static function getMappingsForHotel($idHotel)
{
return self::getConnectedProductIds($idHotel);
}
}

View File

@@ -0,0 +1,541 @@
<?php
/**
* 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
*/
if (!defined('_PS_VERSION_')) {
exit;
}
class QghcSdmService
{
/** Fallback check-in/check-out time-of-day when a hotel hasn't set its own. */
const DEFAULT_CHECKIN_TIME = '15:00:00';
const DEFAULT_CHECKOUT_TIME = '11:00:00';
/**
* Builds hotel SDM data for the hotel landing/category page.
*
* @param Context $context
* @return array|null schema.org/Hotel array, or null if not applicable.
*/
public static function buildLandingPageData(Context $context)
{
$idCategory = (int)Tools::getValue('id_category');
if (!$idCategory) {
return null;
}
$idHotel = (int)HotelBranchInformation::getHotelIdByIdCategory($idCategory);
if (!$idHotel) {
return null;
}
$dateFrom = Tools::getValue('date_from');
$dateTo = Tools::getValue('date_to');
if (!$dateFrom || !$dateTo) {
return null;
}
$tsFrom = strtotime($dateFrom);
$tsTo = strtotime($dateTo);
if (false === $tsFrom || false === $tsTo || $tsTo <= $tsFrom) {
return null;
}
$dateFrom = date('Y-m-d', $tsFrom);
$dateTo = date('Y-m-d', $tsTo);
$idLang = (int)$context->language->id;
$hotelName = QghcPropertyService::getHotelName($idHotel, $idLang);
$hotelInfo = new HotelBranchInformation($idHotel, $idLang);
$countryIso = $hotelInfo->id_country ? Country::getIsoById((int)$hotelInfo->id_country) : '';
$roomTypes = QghcPropertyService::getHotelRoomTypesForSdm($idHotel, $idLang);
if (empty($roomTypes)) {
return null;
}
$checkinDateTime = $dateFrom . 'T' . self::resolveTimeOfDay($hotelInfo->check_in, self::DEFAULT_CHECKIN_TIME);
$checkoutDateTime = $dateTo . 'T' . self::resolveTimeOfDay($hotelInfo->check_out, self::DEFAULT_CHECKOUT_TIME);
$objBookingDetail = new HotelBookingDetail();
$availData = $objBookingDetail->dataForFrontSearch(array(
'date_from' => $dateFrom,
'date_to' => $dateTo,
'hotel_id' => $idHotel,
'only_search_data' => 1,
));
$currency = QghcPropertyService::getDefaultCurrencyIsoCode();
$containsPlace = array();
foreach ($roomTypes as $rt) {
$rtIdProduct = (int)$rt['id_product'];
$isAvailable = !empty($availData['rm_data'][$rtIdProduct]['data']['available']);
$offer = array(
'@type' => array('Offer', 'LodgingReservation'),
'checkinTime' => $checkinDateTime,
'checkoutTime' => $checkoutDateTime,
'availability' => $isAvailable
? 'https://schema.org/InStock'
: 'https://schema.org/SoldOut',
);
if ($isAvailable) {
$priceData = QghcPropertyService::getRoomTypeFinalPriceForSdm(
$rtIdProduct,
$dateFrom,
$dateTo,
1
);
if (!empty($priceData['total_price_tax_incl'])) {
$offer['priceSpecification'] = array(
'@type' => 'CompoundPriceSpecification',
'price' => round((float)$priceData['total_price_tax_incl'], 2),
'priceCurrency' => $currency,
);
}
}
$roomIdentifier = (!empty($rt['id_google_room_type']))
? (string)$rt['id_google_room_type']
: (string)$rtIdProduct;
$roomEntry = array(
'@type' => array('HotelRoom', 'Product'),
'identifier' => $roomIdentifier,
'name' => $rt['room_name'],
);
$occupancy = self::buildOccupancy($rt);
if (null !== $occupancy) {
$roomEntry['occupancy'] = $occupancy;
}
$roomEntry['offers'] = $offer;
$containsPlace[] = $roomEntry;
}
if (empty($containsPlace)) {
return null;
}
$makesOffer = self::buildMakesOffer($containsPlace, $checkinDateTime, $checkoutDateTime);
return self::buildHotelSdmArray($idHotel, $hotelName, $hotelInfo, $countryIso, $containsPlace, $makesOffer);
}
/**
* Builds hotel SDM for the hotel landing/category page, as Microdata.
*
* @param Context $context
* @return string Hidden Microdata markup, or empty string if not applicable.
*/
public static function buildLandingPageMicrodata(Context $context)
{
$data = self::buildLandingPageData($context);
return $data ? self::wrapMicrodata($data) : '';
}
/**
* Builds hotel SDM data for each hotel in the checkout cart.
*
* @param Context $context
* @return array List of schema.org/Hotel arrays, one per hotel in the cart.
*/
public static function buildCheckoutData(Context $context)
{
$idCart = (int)$context->cart->id;
if (!$idCart) {
return array();
}
$objCartData = new HotelCartBookingData();
$cartRows = $objCartData->getCartCurrentDataByCartId($idCart);
if (empty($cartRows)) {
return array();
}
$idLang = (int)$context->language->id;
$currency = QghcPropertyService::getDefaultCurrencyIsoCode();
$hotelProducts = array();
foreach ($cartRows as $row) {
$idHotel = (int)$row['id_hotel'];
$idProduct = (int)$row['id_product'];
$dateKey = $row['date_from'] . '|' . $row['date_to'];
if (!isset($hotelProducts[$idHotel][$idProduct][$dateKey])) {
$hotelProducts[$idHotel][$idProduct][$dateKey] = array(
'date_from' => $row['date_from'],
'date_to' => $row['date_to'],
'qty' => 0,
);
}
$hotelProducts[$idHotel][$idProduct][$dateKey]['qty']++;
}
$hotels = array();
foreach ($hotelProducts as $idHotel => $products) {
$hotelName = QghcPropertyService::getHotelName($idHotel, $idLang);
$hotelInfo = new HotelBranchInformation($idHotel, $idLang);
$countryIso = $hotelInfo->id_country ? Country::getIsoById((int)$hotelInfo->id_country) : '';
// Only room types actually enrolled in Google Hotel may appear in the markup.
$enrolledRooms = array();
foreach (QghcPropertyService::getHotelRoomTypesForSdm($idHotel, $idLang) as $enrolledRow) {
$enrolledRooms[(int)$enrolledRow['id_product']] = $enrolledRow;
}
$containsPlace = array();
foreach ($products as $idProduct => $dateGroups) {
if (!isset($enrolledRooms[$idProduct])) {
continue;
}
$roomName = $enrolledRooms[$idProduct]['room_name'];
$occupancy = self::buildOccupancy($enrolledRooms[$idProduct]);
foreach ($dateGroups as $info) {
$dateFrom = date('Y-m-d', strtotime($info['date_from']));
$dateTo = date('Y-m-d', strtotime($info['date_to']));
$qty = (int)$info['qty'];
$checkinDateTime = $dateFrom . 'T' . self::resolveTimeOfDay($hotelInfo->check_in, self::DEFAULT_CHECKIN_TIME);
$checkoutDateTime = $dateTo . 'T' . self::resolveTimeOfDay($hotelInfo->check_out, self::DEFAULT_CHECKOUT_TIME);
$priceData = QghcPropertyService::getRoomTypeFinalPriceForSdm(
$idProduct,
$dateFrom,
$dateTo,
$qty
);
$offer = array(
'@type' => array('Offer', 'LodgingReservation'),
'checkinTime' => $checkinDateTime,
'checkoutTime' => $checkoutDateTime,
'availability' => 'https://schema.org/InStock',
);
if (!empty($priceData['total_price_tax_incl'])) {
$offer['priceSpecification'] = array(
'@type' => 'CompoundPriceSpecification',
'price' => round((float)$priceData['total_price_tax_incl'], 2),
'priceCurrency' => $currency,
);
}
$roomIdentifier = (!empty($enrolledRooms[$idProduct]['id_google_room_type']))
? (string)$enrolledRooms[$idProduct]['id_google_room_type']
: (string)$idProduct;
$roomEntry = array(
'@type' => array('HotelRoom', 'Product'),
'identifier' => $roomIdentifier,
'name' => $roomName,
);
if (null !== $occupancy) {
$roomEntry['occupancy'] = $occupancy;
}
$roomEntry['offers'] = $offer;
$containsPlace[] = $roomEntry;
}
}
if (empty($containsPlace)) {
continue;
}
$lastCheckin = $containsPlace[count($containsPlace) - 1]['offers']['checkinTime'];
$lastCheckout = $containsPlace[count($containsPlace) - 1]['offers']['checkoutTime'];
$makesOffer = self::buildMakesOffer($containsPlace, $lastCheckin, $lastCheckout);
$hotels[] = self::buildHotelSdmArray($idHotel, $hotelName, $hotelInfo, $countryIso, $containsPlace, $makesOffer);
}
return $hotels;
}
/**
* Builds hotel SDM for each hotel in the checkout cart, as Microdata.
*
* @param Context $context
* @return string One or more hidden Microdata blocks, or empty string.
*/
public static function buildCheckoutMicrodata(Context $context)
{
$output = '';
foreach (self::buildCheckoutData($context) as $hotelData) {
$output .= self::wrapMicrodata($hotelData);
}
return $output;
}
/**
* Assembles a schema.org/Hotel array — the data wrapMicrodata() serializes.
*
* @param int $idHotel
* @param string $hotelName
* @param HotelBranchInformation $hotelInfo
* @param string $countryIso
* @param array $containsPlace
* @param array $makesOffer Hotel-level Offer/LodgingReservation (required by Google's spec).
* @return array
*/
public static function buildHotelSdmArray($idHotel, $hotelName, $hotelInfo, $countryIso, $containsPlace, $makesOffer)
{
$postalAddress = array('@type' => 'PostalAddress');
if ($hotelInfo->address) {
$postalAddress['streetAddress'] = $hotelInfo->address;
}
if ($hotelInfo->city) {
$postalAddress['addressLocality'] = $hotelInfo->city;
}
if (!empty($hotelInfo->id_country) && !empty($hotelInfo->id_state)
&& Country::containsStates((int)$hotelInfo->id_country)
) {
$stateName = State::getNameById((int)$hotelInfo->id_state);
if ($stateName) {
$postalAddress['addressRegion'] = $stateName;
}
}
if ($hotelInfo->zipcode) {
$postalAddress['postalCode'] = $hotelInfo->zipcode;
}
if ($countryIso) {
$postalAddress['addressCountry'] = $countryIso;
}
$idGoogleHotel = QghcPropertyService::getGoogleHotelId((int)$idHotel);
return array(
'@context' => 'https://schema.org',
'@type' => 'Hotel',
'name' => $hotelName,
'identifier' => $idGoogleHotel ? $idGoogleHotel : (string)$idHotel,
'address' => $postalAddress,
'containsPlace' => $containsPlace,
'makesOffer' => $makesOffer,
);
}
/**
* Builds the required Hotel-level `makesOffer` from the lowest-priced available
* room, falling back to SoldOut if nothing is available.
*
* @param array $containsPlace
* @param string $checkinDateTime
* @param string $checkoutDateTime
* @return array
*/
private static function buildMakesOffer($containsPlace, $checkinDateTime, $checkoutDateTime)
{
$lowestPrice = null;
$currency = null;
foreach ($containsPlace as $room) {
$roomOffer = $room['offers'];
if (!isset($roomOffer['priceSpecification']['price'])) {
continue;
}
$price = (float)$roomOffer['priceSpecification']['price'];
if (null === $lowestPrice || $price < $lowestPrice) {
$lowestPrice = $price;
$currency = $roomOffer['priceSpecification']['priceCurrency'];
}
}
$makesOffer = array(
'@type' => array('Offer', 'LodgingReservation'),
'checkinTime' => $checkinDateTime,
'checkoutTime' => $checkoutDateTime,
'availability' => (null !== $lowestPrice)
? 'https://schema.org/InStock'
: 'https://schema.org/SoldOut',
);
if (null !== $lowestPrice) {
$makesOffer['priceSpecification'] = array(
'@type' => 'CompoundPriceSpecification',
'price' => $lowestPrice,
'priceCurrency' => $currency,
);
}
return $makesOffer;
}
/**
* Builds HotelRoom.occupancy from a room type's max_adults/max_children capacity.
*
* @param array $roomRow Row from QghcPropertyService::getHotelRoomTypesForSdm().
* @return array|null Null if the room type has no capacity configured.
*/
private static function buildOccupancy($roomRow)
{
$capacity = (int)($roomRow['max_adults'] ?? 0) + (int)($roomRow['max_children'] ?? 0);
if ($capacity <= 0) {
return null;
}
return array(
'@type' => 'QuantitativeValue',
'value' => $capacity,
);
}
/**
* Resolves a stored "HH:MM"-style hotel check-in/check-out time to "HH:MM:SS",
* falling back to a sane industry-default when the hotel hasn't set one.
*
* @param string|null $rawTime
* @param string $fallback "HH:MM:SS"
* @return string
*/
private static function resolveTimeOfDay($rawTime, $fallback)
{
if ($rawTime && false !== strtotime($rawTime)) {
return date('H:i:s', strtotime($rawTime));
}
return $fallback;
}
/**
* Renders the same SDM array as hidden Microdata (itemscope/itemtype/itemprop) —
* hidden the same way a JSON-LD <script> tag is: never rendered visually, read the
* same way by Google's structured-data parser either way. Emitted in the body (not
* <head>, where a <div> is not valid content) via hookDisplayFooterBefore.
*
* @param array $sdm A schema.org/Hotel array from buildHotelSdmArray().
* @return string
*/
public static function wrapMicrodata($sdm)
{
$html = '<div itemscope itemtype="https://schema.org/Hotel" style="display:none">';
$html .= self::microdataMeta('identifier', $sdm['identifier']);
$html .= self::microdataText('name', $sdm['name']);
$html .= self::microdataAddress($sdm['address']);
foreach ($sdm['containsPlace'] as $room) {
$html .= self::microdataRoom($room);
}
$html .= self::microdataOffer('makesOffer', $sdm['makesOffer']);
$html .= '</div>';
return $html;
}
/** @return string A <div itemprop="address" itemscope itemtype=".../PostalAddress"> block. */
private static function microdataAddress($address)
{
$html = '<div itemprop="address" itemscope itemtype="https://schema.org/PostalAddress">';
if (isset($address['streetAddress'])) {
$html .= self::microdataText('streetAddress', $address['streetAddress']);
}
if (isset($address['addressLocality'])) {
$html .= self::microdataText('addressLocality', $address['addressLocality']);
}
if (isset($address['addressRegion'])) {
$html .= self::microdataText('addressRegion', $address['addressRegion']);
}
if (isset($address['postalCode'])) {
$html .= self::microdataText('postalCode', $address['postalCode']);
}
if (isset($address['addressCountry'])) {
$html .= self::microdataText('addressCountry', $address['addressCountry']);
}
$html .= '</div>';
return $html;
}
/** @return string A <div itemprop="containsPlace" itemscope itemtype=".../HotelRoom .../Product"> block.
* Dual-typed HotelRoom+Product to match Google's own official Hotel price
* structured-data Microdata example verbatim. This also makes Google's unrelated
* Merchant Listings feature evaluate the same node against product-feed fields
* (gtin/brand/shippingDetails/etc.) that don't apply to a hotel room and will show
* as "invalid" there — that is expected and does not affect Hotels-feature
* price-accuracy validation, which is what this module is certified against. Do not
* drop the Product type to silence Merchant Listings; that breaks containsPlace's
* type for the Hotels feature instead (confirmed via Rich Results Test). */
private static function microdataRoom($room)
{
$html = '<div itemprop="containsPlace" itemscope itemtype="https://schema.org/HotelRoom https://schema.org/Product">';
$html .= self::microdataMeta('identifier', $room['identifier']);
$html .= self::microdataText('name', $room['name']);
if (isset($room['occupancy'])) {
$html .= '<div itemprop="occupancy" itemscope itemtype="https://schema.org/QuantitativeValue">';
$html .= self::microdataMeta('value', $room['occupancy']['value']);
$html .= '</div>';
}
$html .= self::microdataOffer('offers', $room['offers']);
$html .= '</div>';
return $html;
}
/** @return string A <div itemprop="$itemprop" itemscope itemtype=".../Offer .../LodgingReservation"> block. */
private static function microdataOffer($itemprop, $offer)
{
$html = '<div itemprop="' . self::esc($itemprop) . '" itemscope itemtype="https://schema.org/Offer https://schema.org/LodgingReservation">';
$html .= self::microdataMeta('checkinTime', $offer['checkinTime']);
$html .= self::microdataMeta('checkoutTime', $offer['checkoutTime']);
$html .= '<link itemprop="availability" href="' . self::esc($offer['availability']) . '">';
if (isset($offer['priceSpecification'])) {
$html .= '<div itemprop="priceSpecification" itemscope itemtype="https://schema.org/CompoundPriceSpecification">';
$html .= self::microdataMeta('price', $offer['priceSpecification']['price']);
$html .= self::microdataMeta('priceCurrency', $offer['priceSpecification']['priceCurrency']);
// Every price this module emits is built from total_price_tax_incl (see
// QghcPropertyService::getRoomTypeFinalPriceForSdm()), so this is always true.
$html .= self::microdataMeta('valueAddedTaxIncluded', 'True');
$html .= '</div>';
}
$html .= '</div>';
return $html;
}
/** @return string A <meta itemprop="$itemprop" content="$value"> tag — valid HTML5 flow content when itemprop is set. */
private static function microdataMeta($itemprop, $value)
{
return '<meta itemprop="' . self::esc($itemprop) . '" content="' . self::esc($value) . '">';
}
/** @return string A <span itemprop="$itemprop">$value</span> tag for text that would normally be visible. */
private static function microdataText($itemprop, $value)
{
return '<span itemprop="' . self::esc($itemprop) . '">' . self::esc($value) . '</span>';
}
private static function esc($value)
{
return htmlspecialchars((string)$value, ENT_QUOTES, 'UTF-8');
}
}

View File

@@ -0,0 +1,200 @@
<?php
/**
* 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
*/
if (!defined('_PS_VERSION_')) {
exit;
}
/**
* Handles PrestaShop webservice key creation and deletion for the QGHC module.
* Separated from the main class to keep module class free of business logic.
*/
class QghcWebserviceSetup
{
const WS_ACCOUNT_DESCRIPTION = 'Google Hotel Configuration Key';
/** Set true only around this class's own delete() calls so the module's protection
* guard (QloGoogleHotelConnector::guardWebserviceKeyObject()) lets them through —
* every other caller must still be blocked. */
public static $bypassGuard = false;
/**
* Creates a PS webservice key for QGHC, granted full permissions on every
* webservice resource (the Channel Manager needs broader access, not just
* ghc_api). Skips creation when a valid key already exists.
*
* @return bool
*/
public static function createKey()
{
$existingKey = (string) Db::getInstance()->getValue(
'SELECT `key` FROM `' . _DB_PREFIX_ . 'webservice_account`
WHERE `description` = \'' . pSQL(self::WS_ACCOUNT_DESCRIPTION) . '\''
);
if ($existingKey) {
return true;
}
return (bool) self::createKeyRow();
}
/**
* Deletes the existing QGHC key (if any) and creates a fresh one — unlike createKey(),
* always generates a new key even when one already exists.
*
* @return string|false New key on success, false on failure.
*/
public static function regenerateKey()
{
if (!self::deleteKey()) {
return false;
}
return self::createKeyRow();
}
/**
* Creates the webservice_account row, grants full permissions on every resource, and
* enables PS_WEBSERVICE if needed. Shared by createKey() and regenerateKey().
*
* @return string|false New key on success, false on failure.
*/
private static function createKeyRow()
{
$key = Tools::strtoupper(Tools::passwdGen(32));
$wsAccount = new WebserviceKey();
$wsAccount->key = $key;
$wsAccount->description = self::WS_ACCOUNT_DESCRIPTION;
$wsAccount->active = 1;
if (!$wsAccount->save()) {
return false;
}
Configuration::updateValue(QloGoogleHotelConnector::CONFIG_WS_KEY, $key);
$resources = WebserviceRequest::getResources();
// ghc_api can be missing here during install due to hook-timing; ensure it anyway.
if (!isset($resources['ghc_api'])) {
$resources['ghc_api'] = array('description' => 'Google Hotel Connector API');
}
foreach ($resources as $resourceName => $resourceDef) {
$forbidden = isset($resourceDef['forbidden_method']) ? $resourceDef['forbidden_method'] : array();
foreach (array('GET', 'POST', 'PUT', 'DELETE', 'HEAD') as $method) {
if (in_array($method, $forbidden, true)) {
continue;
}
Db::getInstance()->insert(
'webservice_permission',
array(
'resource' => pSQL($resourceName),
'method' => $method,
'id_webservice_account' => (int) $wsAccount->id,
),
false,
true,
Db::INSERT_IGNORE
);
}
}
if (!Configuration::get('PS_WEBSERVICE')) {
Configuration::updateValue('PS_WEBSERVICE', 1);
}
return $key;
}
/**
* Returns true when the given key exists in the PS webservice accounts table.
*
* @param string $key
* @return bool
*/
public static function keyExists($key)
{
return (bool) Db::getInstance()->getValue(
'SELECT COUNT(*) FROM `' . _DB_PREFIX_ . 'webservice_account`
WHERE `key` = \'' . pSQL($key) . '\''
);
}
/**
* Returns the active QGHC webservice key, or empty string if none exists.
*
* @return string
*/
public static function getKey()
{
$key = (string) Configuration::get(QloGoogleHotelConnector::CONFIG_WS_KEY);
if (!$key) {
$key = (string) Db::getInstance()->getValue(
'SELECT `key` FROM `' . _DB_PREFIX_ . 'webservice_account`
WHERE `description` = \'' . pSQL(self::WS_ACCOUNT_DESCRIPTION) . '\' AND `active` = 1'
);
}
return $key;
}
/**
* Deletes the QGHC webservice account from PrestaShop.
*
* @return bool
*/
public static function deleteKey()
{
$key = (string) Configuration::get(QloGoogleHotelConnector::CONFIG_WS_KEY);
if (!$key) {
$key = (string) Db::getInstance()->getValue(
'SELECT `key` FROM `' . _DB_PREFIX_ . 'webservice_account`
WHERE `description` = \'' . pSQL(self::WS_ACCOUNT_DESCRIPTION) . '\''
);
}
if (!$key) {
return true;
}
$idAccount = (int) Db::getInstance()->getValue(
'SELECT `id_webservice_account` FROM `' . _DB_PREFIX_ . 'webservice_account`
WHERE `key` = \'' . pSQL($key) . '\''
);
if ($idAccount) {
$wsAccount = new WebserviceKey($idAccount);
self::$bypassGuard = true;
try {
$deleted = $wsAccount->delete();
} catch (Exception $e) {
self::$bypassGuard = false;
throw $e;
}
self::$bypassGuard = false;
if (!$deleted) {
return false;
}
}
return true;
}
}

View File

@@ -0,0 +1,219 @@
<?php
/**
* 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
*/
if (!class_exists('QghcProperty')) {
require_once dirname(__FILE__) . '/QghcRequiredClasses.php';
}
class WebserviceSpecificManagementGhcApi implements WebserviceSpecificManagementInterface
{
protected $objOutput;
protected $output;
protected $wsObject;
private $logApiType = QghcApiLog::TYPE_UNKNOWN;
private $logIdHotel = 0;
private $logStatus = true;
private $logMessage = '';
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()
{
$method = $this->wsObject->method;
$endpoint = isset($this->wsObject->urlSegment[1]) ? $this->wsObject->urlSegment[1] : '';
if (in_array($method, array('POST', 'PUT', 'PATCH'), true)) {
$rawBody = file_get_contents('php://input');
} else {
// GET requests (e.g. the CM fetching properties/ARI to verify the connection)
// carry their data in the query string, not a body — log that instead, or the
// Request column is always empty for every GET call. ws_key is PS's webservice
// auth fallback (see webservice/dispatcher.php) — never log it.
$queryParams = $_GET;
unset($queryParams['ws_key']);
$rawBody = $queryParams ? json_encode($queryParams, JSON_UNESCAPED_SLASHES) : '';
}
try {
if ($method === 'GET' && $endpoint === 'google-ari') {
$this->logApiType = (int)Tools::getValue('only_change', 0) === 1 ? QghcApiLog::TYPE_ARI_CHANGE : QghcApiLog::TYPE_ARI_FULL;
$filter = Tools::getValue('filter', array());
$allowedFilters = array(
'id_property' => 'int',
'id_room_type' => 'string',
'date_from' => 'string',
'date_to' => 'string',
);
$invalidFilterKey = null;
foreach ($filter as $filterKey => $filterValue) {
if (isset($allowedFilters[$filterKey]) && is_array($filterValue)) {
$invalidFilterKey = $filterKey;
break;
}
}
if ($invalidFilterKey !== null) {
$this->output = json_encode(array('success' => false, 'error' => 'Invalid filter: ' . $invalidFilterKey), JSON_UNESCAPED_SLASHES);
$this->logStatus = false;
$this->logMessage = 'Invalid filter: ' . $invalidFilterKey;
} elseif (!isset($filter['id_property']) || !filter_var($filter['id_property'], FILTER_VALIDATE_INT)) {
$this->output = json_encode(array('success' => false, 'error' => 'id_property filter is required'), JSON_UNESCAPED_SLASHES);
$this->logStatus = false;
$this->logMessage = 'id_property filter is required';
} else {
$this->logIdHotel = (int)$filter['id_property'];
$objAri = new QghcAri();
$this->output = $this->logApiType === QghcApiLog::TYPE_ARI_CHANGE ? $objAri->getAriChange($filter) : $objAri->getAriByIdProperty($filter);
}
} elseif ($method === 'GET' && $endpoint === 'google-hotel-properties') {
$this->logApiType = QghcApiLog::TYPE_HOTEL_LIST_FEED;
$this->output = QghcPropertyService::getPropertiesFeed();
} elseif ($method === 'POST' && $endpoint === 'google_status') {
$this->logApiType = QghcApiLog::TYPE_GOOGLE_STATUS;
$input = $this->getPostRequest();
$values = isset($input['google_hotel_values']) && is_array($input['google_hotel_values']) ? $input['google_hotel_values'] : array();
$values['send_mail'] = !empty($input['send_mail']);
if (empty($values['properties']) || !is_array($values['properties'])) {
$this->output = json_encode(array('success' => false, 'error' => 'properties map is required in google_hotel_values'), JSON_UNESCAPED_SLASHES);
$this->logStatus = false;
$this->logMessage = 'properties map is required in google_hotel_values';
} else {
$result = QghcPropertyService::applyGoogleStatusUpdate($values);
if (empty($result['updated_properties'])) {
$this->output = json_encode(array('success' => false, 'error' => 'No matching properties found to update'), JSON_UNESCAPED_SLASHES);
$this->logStatus = false;
$this->logMessage = 'No matching properties found to update';
} else {
$this->logIdHotel = $result['updated_properties'][0];
$this->output = json_encode(array(
'success' => true,
'google_status' => $result['google_status'],
'updated_properties' => $result['updated_properties'],
'updated_room_types' => $result['updated_room_types'],
));
}
}
} elseif (!in_array($method, array('GET', 'POST'), true)) {
// A raw header() call here is overwritten later by dispatcher.php's own
// header loop (WebserviceOutputBuilder defaults to 200 OK) — setStatus()
// is the only way to actually change the response's HTTP status code.
$this->objOutput->setStatus(405);
$this->output = json_encode(array('success' => false, 'error' => 'HTTP method not allowed'), JSON_UNESCAPED_SLASHES);
$this->logStatus = false;
$this->logMessage = 'HTTP method not allowed';
} else {
$this->output = json_encode(array('success' => false, 'error' => 'Unknown endpoint'), JSON_UNESCAPED_SLASHES);
$this->logStatus = false;
$this->logMessage = 'Unknown endpoint';
}
} catch (\Exception $e) {
$this->output = json_encode(array('success' => false, 'error' => 'Internal error, please contact support.'), JSON_UNESCAPED_SLASHES);
$this->logStatus = false;
$this->logMessage = $e->getMessage();
}
$response = (string) $this->output;
// DB Log started
QghcApiLog::insert(
$this->logIdHotel,
$this->logApiType,
$rawBody,
$response,
$this->logStatus ? QghcApiLog::STATUS_SUCCESS : QghcApiLog::STATUS_ERROR,
$this->logMessage
);
// DB Log END
// File Log started
$logDir = _PS_MODULE_DIR_ . 'qlogooglehotelconnector/logs/';
if (!is_dir($logDir)) {
@mkdir($logDir, 0755, true);
}
$data = date('[Y-m-d H:i e] ') . PHP_EOL;
$data .= 'Url : ghc_api/' . $endpoint . PHP_EOL;
$data .= 'Method : ' . $method . PHP_EOL;
$data .= 'Request : ' . $rawBody . PHP_EOL;
$data .= 'Response: ' . $response . PHP_EOL;
$data .= '___________________________________________';
$data .= PHP_EOL . PHP_EOL;
error_log($data, 3, $logDir . date('Y-m-d') . '.log');
// File Log END
return $this->wsObject->getOutputEnabled();
}
public function getContent()
{
$content = $this->output ? $this->output : json_encode(array());
$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
);
return $content;
}
private function getPostRequest()
{
$rawBody = file_get_contents('php://input');
$data = json_decode($rawBody, true);
return is_array($data) ? $data : array();
}
}

View File

@@ -0,0 +1,32 @@
<?php
/**
* 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
*/
header('Expires: Mon, 26 Jul 1997 05:00:00 GMT');
header('Last-Modified: '.gmdate('D, d M Y H:i:s').' GMT');
header('Cache-Control: no-store, no-cache, must-revalidate');
header('Cache-Control: post-check=0, pre-check=0', false);
header('Pragma: no-cache');
header('Location: ../../../');
exit;

View File

@@ -0,0 +1,245 @@
<?php
/**
* 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
*/
if (!defined('_PS_VERSION_')) {
exit;
}
require_once _PS_MODULE_DIR_ . 'qlogooglehotelconnector/classes/QghcRequiredClasses.php';
class AdminGoogleHotelConfigController extends ModuleAdminController
{
/** _conf[] code: connection created successfully. */
const CONF_CONNECTED = 101;
/** _conf[] code: connection removed successfully. */
const CONF_DISCONNECTED = 102;
/** _conf[] code: webservice key regenerated successfully. */
const CONF_KEY_REGENERATED = 103;
public function __construct()
{
$this->bootstrap = true;
parent::__construct();
$this->override_folder = '';
$this->meta_title = $this->l('Google Hotel Connector - Configuration');
// Standard QloApps pattern for a success message that survives the
// post-save redirect (matches blocknewsletter/wkmyallocatorconnector,
// which both extend _conf[] with custom codes above core's own range)
// — core's own alerts.tpl reads and displays these automatically.
$this->_conf[self::CONF_CONNECTED] = $this->l('Connection created successfully.');
$this->_conf[self::CONF_DISCONNECTED] = $this->l('Connection disconnected successfully.');
$this->_conf[self::CONF_KEY_REGENERATED] = $this->l('Webservice key regenerated successfully.');
}
/**
* Runs before initContent(). After any DB write we redirect (PRG pattern)
* so a browser refresh cannot re-submit the form.
*/
public function postProcess()
{
if (Tools::isSubmit('submitConfigurationConnection')) {
$this->processSave();
return;
}
if (Tools::isSubmit('submitDeleteConfigurationConnection')) {
$this->processDelete();
return;
}
if (Tools::isSubmit('submitRegenerateWebserviceKey')) {
$this->processRegenerateKey();
return;
}
}
/** Renders the configuration page into the admin layout. */
public function initContent()
{
parent::initContent();
if (!$this->tabAccess['view']) {
$this->errors[] = Tools::displayError('You do not have permission to view this.');
return;
}
$this->renderForm();
$this->setTemplate('config.tpl');
}
/** Builds all Smarty variables the config template needs; config.tpl is fetched once by display(). */
public function renderForm()
{
$idLang = (int) $this->context->language->id;
$isConnected = (int) Configuration::get(QloGoogleHotelConnector::CONFIG_STATUS) === QghcProperty::GOOGLE_STATUS_CONNECTED;
$wsKey = QghcWebserviceSetup::getKey();
// Named-constant status badges, matching renderStatusBadge()'s mapping in the
// main module class (used for the same google_status value on the hotel list).
$statusBadges = array(
QghcProperty::GOOGLE_STATUS_CONNECTED => array('class' => 'label-success', 'icon' => 'icon-check', 'label' => $this->l('Verified')),
QghcProperty::GOOGLE_STATUS_FAILED => array('class' => 'label-danger', 'icon' => 'icon-times', 'label' => $this->l('Failed')),
);
$pendingBadge = array('class' => 'label-warning', 'icon' => 'icon-clock-o', 'label' => $this->l('Pending'));
$rows = QghcProperty::getEnrolledProperties($idLang);
$configs = array();
$hotelEditBase = $this->context->link->getAdminLink('AdminAddHotel');
if ($rows) {
foreach ($rows as $row) {
$badge = isset($statusBadges[(int)$row['google_status']]) ? $statusBadges[(int)$row['google_status']] : $pendingBadge;
$configs[] = array(
'id_hotel' => (int)$row['id_hotel'],
'hotel_name' => $row['hotel_name'],
'date_add' => $row['date_add'],
'edit_url' => $hotelEditBase . '&id=' . (int)$row['id_hotel'] . '&updatehtl_branch_info',
'status_class' => $badge['class'],
'status_icon' => $badge['icon'],
'status_label' => $badge['label'],
);
}
}
// Token must match the check in ghclogcron.php
$cronUrl = Tools::getShopDomainSsl(true, true) . __PS_BASE_URI__ . 'modules/qlogooglehotelconnector/ghclogcron.php?token=' . Tools::encrypt('qlogooglehotelconnector');
$this->context->smarty->assign(array(
'configs' => $configs,
'ws_key' => $wsKey,
'is_connected' => $isConnected,
'action_url' => $this->context->link->getAdminLink('AdminGoogleHotelConfig'),
'cron_url' => $cronUrl,
'hotel_list_url' => $hotelEditBase,
));
}
/** Validates and saves the webservice key and connection status. Redirects with a success or error code. */
public function processSave()
{
if (!$this->tabAccess['edit']) {
$this->errors[] = Tools::displayError('You do not have permission to edit this.');
return;
}
$wsKey = trim(Tools::getValue(QloGoogleHotelConnector::CONFIG_WS_KEY));
$pmsUrl = Tools::getShopDomainSsl(true, true) . __PS_BASE_URI__;
if (empty($wsKey)) {
$this->errors[] = $this->l('Webservice key is required.');
return;
}
if (!QghcWebserviceSetup::keyExists($wsKey)) {
$this->errors[] = $this->l('Invalid webservice key. The key does not exist in your webservice accounts.');
return;
}
$apiService = new QghcApiService();
$result = $apiService->createConnection($pmsUrl, $wsKey);
$isConnected = (!empty($result['http_code']) && (int) $result['http_code'] === 200) ? 1 : 0;
Configuration::updateValue(QloGoogleHotelConnector::CONFIG_WS_KEY, $wsKey);
Configuration::updateValue(QloGoogleHotelConnector::CONFIG_STATUS, $isConnected ? QghcProperty::GOOGLE_STATUS_CONNECTED : 0);
if ($isConnected) {
Tools::redirectAdmin($this->context->link->getAdminLink('AdminGoogleHotelConfig') . '&conf=' . self::CONF_CONNECTED);
}
// Failure: no redirect (matches core's own pattern — a redirect is only used
// to prevent re-submitting a *successful* save on refresh). $this->errors[]
// renders immediately via alerts.tpl on this same request.
$httpCode = !empty($result['http_code']) ? (int)$result['http_code'] : 0;
if ($httpCode === 0) {
$detail = $this->l('Could not reach Google Hotel Center — check that it is running and accessible.');
} else {
$detail = $this->l('Google Hotel Center returned HTTP') . ' ' . $httpCode . '.';
}
$this->errors[] = $this->l('Failed to connect to Google Hotel Center.') . ' ' . $detail;
}
/** Disconnects from the channel manager. Blocks deletion if any hotels are still linked to Google Hotel. */
public function processDelete()
{
if (!$this->tabAccess['delete']) {
$this->errors[] = Tools::displayError('You do not have permission to delete this.');
return;
}
if (QghcProperty::countHotelsLinkedToGoogle() > 0) {
$this->errors[] = $this->l('You must disable all hotels from Google Hotel before deleting the connection. Please edit each hotel and disable it in the Google Hotel tab.');
return;
}
$wsKey = QghcWebserviceSetup::getKey();
$pmsUrl = Tools::getShopDomainSsl(true, true) . __PS_BASE_URI__;
if ($wsKey !== '') {
$apiService = new QghcApiService();
$result = $apiService->deleteConnection($pmsUrl, $wsKey);
$cmSuccess = !empty($result['http_code']) && (int) $result['http_code'] === 200;
if (!$cmSuccess) {
$this->errors[] = $this->l('Failed to disconnect from Google Hotel Center. Please try again.');
return;
}
}
Configuration::updateValue(QloGoogleHotelConnector::CONFIG_STATUS, 0);
Configuration::deleteByName(QloGoogleHotelConnector::CONFIG_WS_KEY);
Tools::redirectAdmin($this->context->link->getAdminLink('AdminGoogleHotelConfig') . '&conf=' . self::CONF_DISCONNECTED);
}
/** Regenerates the QGHC webservice key and re-registers it with the channel manager. */
public function processRegenerateKey()
{
if (!$this->tabAccess['edit']) {
$this->errors[] = Tools::displayError('You do not have permission to edit this.');
return;
}
$newKey = QghcWebserviceSetup::regenerateKey();
if (!$newKey) {
$this->errors[] = $this->l('Failed to regenerate the webservice key.');
return;
}
$pmsUrl = Tools::getShopDomainSsl(true, true) . __PS_BASE_URI__;
$result = (new QghcApiService())->createConnection($pmsUrl, $newKey);
$isConnected = !empty($result['http_code']) && (int) $result['http_code'] === 200;
Configuration::updateValue(QloGoogleHotelConnector::CONFIG_WS_KEY, $newKey);
Configuration::updateValue(QloGoogleHotelConnector::CONFIG_STATUS, $isConnected ? QghcProperty::GOOGLE_STATUS_CONNECTED : 0);
if ($isConnected) {
Tools::redirectAdmin($this->context->link->getAdminLink('AdminGoogleHotelConfig') . '&conf=' . self::CONF_KEY_REGENERATED);
}
$httpCode = !empty($result['http_code']) ? (int) $result['http_code'] : 0;
$detail = $httpCode === 0
? $this->l('Could not reach Google Hotel Center — check that it is running and accessible.')
: $this->l('Google Hotel Center returned HTTP') . ' ' . $httpCode . '.';
$this->errors[] = $this->l('The webservice key was regenerated, but Google Hotel Center could not be notified.') . ' ' . $detail;
}
}

View File

@@ -0,0 +1,293 @@
<?php
/**
* 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
*/
if (!defined('_PS_VERSION_')) {
exit;
}
require_once _PS_MODULE_DIR_ . 'qlogooglehotelconnector/classes/QghcRequiredClasses.php';
class AdminGoogleHotelLogsController extends ModuleAdminController
{
private $typeLabels = array();
public function __construct()
{
// Every QghcApiLog::TYPE_* must have an entry here, or it shows as its raw
// value in the filter dropdown, the list column, and the detail view.
$this->typeLabels = array(
QghcApiLog::TYPE_ARI_FULL => 'Full Availability & Rates Update',
QghcApiLog::TYPE_ARI_CHANGE => 'Availability & Rates Change Update',
QghcApiLog::TYPE_GOOGLE_STATUS => 'Google Status',
QghcApiLog::TYPE_CONNECTION => 'Connection',
QghcApiLog::TYPE_DELETE_CONNECTION => 'Delete Connection',
QghcApiLog::TYPE_HOTEL_LIST_FEED => 'Property Detail',
);
$this->bootstrap = true;
$this->table = 'qghc_api_log';
$this->className = 'QghcApiLog';
$this->identifier = 'id_qghc_api_log';
$this->lang = false;
$this->noLink = true;
$this->list_no_link = true;
$this->_orderBy = 'a.date_add';
$this->_orderWay = 'DESC';
// JOIN hotel name from htl_branch_info_lang; id_hotel = 0 = connection/global log
$idLang = (int) Context::getContext()->language->id;
$this->_select = 'COALESCE(hbl.`hotel_name`, \'\') AS `hotel_name`';
$this->_join = ' LEFT JOIN `' . _DB_PREFIX_ . 'htl_branch_info_lang` hbl' . ' ON hbl.`id` = a.`id_hotel` AND hbl.`id_lang` = ' . $idLang;
// All real hotels for the Hotel filter (no Global/id=0 entry — that's not a hotel)
$allHotels = QghcProperty::getAllHotels($idLang);
$hotelList = array();
if ($allHotels) {
foreach ($allHotels as $row) {
$hotelList[(int)$row['id']] = $row['hotel_name'];
}
}
$this->fields_list = array(
'id_qghc_api_log' => array(
'title' => 'ID',
'align' => 'center',
'class' => 'fixed-width-xs',
),
'id_hotel' => array(
'title' => 'Hotel',
'type' => 'select',
'filter_key' => 'a!id_hotel',
'list' => $hotelList,
'callback' => 'formatHotel',
'callback_object' => $this,
),
'api_type' => array(
'title' => 'Log Type',
'type' => 'select',
'filter_key' => 'a!api_type',
'list' => $this->typeLabels,
'callback' => 'formatType',
'callback_object' => $this,
),
'status' => array(
'title' => 'Status',
'align' => 'center',
'type' => 'select',
'filter_key' => 'a!status',
'list' => array(
QghcApiLog::STATUS_SUCCESS => 'Success',
QghcApiLog::STATUS_ERROR => 'Error',
),
'callback' => 'formatStatus',
'callback_object' => $this,
),
'date_add' => array(
'title' => 'Date',
'type' => 'datetime',
'align' => 'right',
'filter_key' => 'a!date_add',
),
);
parent::__construct();
$this->override_folder = '';
$this->meta_title = $this->l('Google Hotel Connector - API Logs');
$this->addRowAction('view');
}
public function initContent()
{
parent::initContent();
$this->setTemplate('logs.tpl');
}
public function renderList()
{
if (!$this->tabAccess['view']) {
$this->errors[] = Tools::displayError('You do not have permission to view this.');
return '';
}
unset($this->toolbar_btn['new']);
return parent::renderList();
}
public function renderView()
{
if (!$this->tabAccess['view']) {
$this->errors[] = Tools::displayError('You do not have permission to view this.');
return '';
}
// core's initContent() already calls loadObject(true) before renderView() for
// display=view and adds its own "cannot be loaded" error to $this->errors — don't
// re-check/re-announce the same thing, just bail without a second, duplicate error.
if (!Validate::isLoadedObject($this->object)) {
return '';
}
$idLogHotel = (int) $this->object->id_hotel;
$hotelName = $idLogHotel ? QghcPropertyService::getHotelName($idLogHotel, (int) $this->context->language->id) : '';
$log = array(
'id_qghc_api_log' => (int) $this->object->id,
'id_hotel' => $idLogHotel,
'api_type' => $this->object->api_type,
'status' => (int) $this->object->status,
'message' => $this->object->message,
'date_add' => $this->object->date_add,
'request' => $this->object->request,
'response' => $this->object->response,
);
$log['type_label'] = isset($this->typeLabels[$log['api_type']]) ? $this->typeLabels[$log['api_type']] : $log['api_type'];
// Same fallback convention as formatHotel() in the list view: no separate
// existence check, shows the raw ID if the hotel was since deleted instead
// of hiding the log entirely.
$log['hotel_label'] = $idLogHotel ? ($hotelName !== '' ? $hotelName : '#' . $idLogHotel) : $this->l('Global Setting');
foreach (array('request', 'response') as $field) {
if (!empty($log[$field])) {
$decoded = json_decode($log[$field], true);
if (is_array($decoded)) {
// If 'body' was stored as a raw JSON string, decode it for display
if (isset($decoded['body']) && is_string($decoded['body'])) {
$bodyDecoded = json_decode($decoded['body'], true);
if (is_array($bodyDecoded)) {
$decoded['body'] = $bodyDecoded;
}
}
$log[$field] = json_encode($decoded, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE);
}
}
}
$this->context->smarty->assign(array(
'ghc_log' => $log,
'back_url' => $this->context->link->getAdminLink('AdminGoogleHotelLogs'),
));
return $this->createTemplate('log_view.tpl')->fetch();
}
/**
* Same as AdminController::addFiltersToBreadcrumbs() except it also resolves
* 'select'-type filters (id_hotel, api_type, status) to their label via the
* field's own 'list' array. Core only special-cases bool/date/datetime/range —
* for 'select' it falls through to the raw stored value (e.g. "ari_full" or "3"
* instead of "Full Availability & Rates Update" or the hotel name).
*/
public function addFiltersToBreadcrumbs()
{
if ($this->filter && is_array($this->fields_list)) {
$filters = array();
foreach ($this->fields_list as $field => $t) {
if (isset($t['filter_key'])) {
$field = $t['filter_key'];
}
if (($val = Tools::getValue($this->table.'Filter_'.$field)) || $val = $this->context->cookie->{$this->getCookieFilterPrefix().$this->table.'Filter_'.$field}) {
if (!is_array($val)) {
$filter_value = '';
if (isset($t['type']) && $t['type'] == 'bool') {
$filter_value = ((bool)$val) ? $this->l('yes') : $this->l('no');
} elseif (isset($t['type']) && $t['type'] == 'date' || isset($t['type']) && $t['type'] == 'datetime') {
$date = json_decode($val, true);
if (isset($date[0])) {
$filter_value = $date[0];
if (isset($date[1]) && !empty($date[1])) {
$filter_value .= ' - '.$date[1];
}
}
} elseif (isset($t['type']) && $t['type'] == 'range') {
$range = json_decode($val, true);
if (isset($range[0]) && !empty($range[0])) {
if (Validate::isUnsignedInt($range[0])) {
$filter_value = $range[0];
if (isset($range[1]) && !empty($range[1])) {
if (Validate::isUnsignedInt($range[1]) && $range[0] < $range[1]) {
$filter_value .= ' - '.$range[1];
}
}
}
} else {
if (isset($range[1]) && !empty($range[1])) {
if (Validate::isUnsignedInt($range[1])) {
$filter_value = $range[1];
}
}
}
} elseif (isset($t['type']) && $t['type'] == 'select' && isset($t['list'][$val])) {
$filter_value = htmlspecialchars($t['list'][$val], ENT_QUOTES, 'UTF-8');
} elseif (is_string($val)) {
$filter_value = htmlspecialchars($val, ENT_QUOTES, 'UTF-8');
}
if (!empty($filter_value)) {
$filters[] = sprintf($this->l('%s: %s'), $t['title'], $filter_value);
}
} else {
$filter_value = '';
foreach ($val as $v) {
if (is_string($v) && !empty($v)) {
$filter_value .= ' - '.htmlspecialchars($v, ENT_QUOTES, 'UTF-8');
}
}
$filter_value = ltrim($filter_value, ' -');
if (!empty($filter_value)) {
$filters[] = sprintf($this->l('%s: %s'), $t['title'], $filter_value);
}
}
}
}
if (count($filters)) {
return sprintf($this->l('Filter by %s'), implode(', ', $filters));
}
}
}
public function formatHotel($value, $row)
{
if (!(int)$value) {
return '<em>' . $this->l('Global Setting') . '</em>';
}
return !empty($row['hotel_name']) ? htmlspecialchars($row['hotel_name'], ENT_QUOTES, 'UTF-8'): (int)$value;
}
public function formatType($value, $row)
{
$label = isset($this->typeLabels[$value]) ? $this->typeLabels[$value] : $value;
return htmlspecialchars($label, ENT_QUOTES, 'UTF-8');
}
public function formatStatus($value, $row)
{
if ((int)$value === QghcApiLog::STATUS_SUCCESS) {
return '<span class="label label-success">' . $this->l('Success') . '</span>';
}
return '<span class="label label-danger">' . $this->l('Error') . '</span>';
}
}

View File

@@ -0,0 +1,32 @@
<?php
/**
* 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
*/
header('Expires: Mon, 26 Jul 1997 05:00:00 GMT');
header('Last-Modified: '.gmdate('D, d M Y H:i:s').' GMT');
header('Cache-Control: no-store, no-cache, must-revalidate');
header('Cache-Control: post-check=0, pre-check=0', false);
header('Pragma: no-cache');
header('Location: ../../../../');
exit;

View File

@@ -0,0 +1,32 @@
<?php
/**
* 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
*/
header('Expires: Mon, 26 Jul 1997 05:00:00 GMT');
header('Last-Modified: '.gmdate('D, d M Y H:i:s').' GMT');
header('Cache-Control: no-store, no-cache, must-revalidate');
header('Cache-Control: post-check=0, pre-check=0', false);
header('Pragma: no-cache');
header('Location: ../../../');
exit;

View File

@@ -0,0 +1,63 @@
<?php
/**
* 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
*/
// Bootstrap PrestaShop (module is 2 levels below shop root: /modules/qlogooglehotelconnector/)
$configPath = dirname(dirname(dirname(__FILE__))) . '/config/config.inc.php';
if (!file_exists($configPath)) {
header('HTTP/1.1 500 Internal Server Error');
die('PrestaShop configuration not found.');
}
require_once $configPath;
// Token validation — must match the module's secure_key
$token = Tools::getValue('token', '');
if ($token !== Tools::encrypt('qlogooglehotelconnector')) {
header('HTTP/1.1 403 Forbidden');
die('Invalid token.');
}
// Delete qghc_api_log rows older than 2 months
$cutoff = date('Y-m-d H:i:s', strtotime('-2 months'));
$deleted = Db::getInstance()->execute(
'DELETE FROM `' . _DB_PREFIX_ . 'qghc_api_log`
WHERE `date_add` < \'' . pSQL($cutoff) . '\''
);
if ($deleted) {
$count = Db::getInstance()->Affected_Rows();
echo 'OK — ' . (int)$count . ' log(s) deleted (older than ' . $cutoff . ').';
} else {
header('HTTP/1.1 500 Internal Server Error');
echo 'ERROR — failed to delete old logs.';
}
// Delete daily file logs older than the same retention window used for the DB rows above.
$logDir = _PS_MODULE_DIR_ . 'qlogooglehotelconnector/logs/';
if (is_dir($logDir)) {
foreach (glob($logDir . '*.log') as $file) {
$fileDate = basename($file, '.log');
if (preg_match('/^\d{4}-\d{2}-\d{2}$/', $fileDate) && strtotime($fileDate) < strtotime($cutoff)) {
@unlink($file);
}
}
}

View File

@@ -0,0 +1,32 @@
<?php
/**
* 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
*/
header('Expires: Mon, 26 Jul 1997 05:00:00 GMT');
header('Last-Modified: '.gmdate('D, d M Y H:i:s').' GMT');
header('Cache-Control: no-store, no-cache, must-revalidate');
header('Cache-Control: post-check=0, pre-check=0', false);
header('Pragma: no-cache');
header('Location: ../../');
exit;

Binary file not shown.

After

Width:  |  Height:  |  Size: 626 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

View File

@@ -0,0 +1,190 @@
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/1999/REC-html401-19991224/strict.dtd">
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=0" />
<title>Message from {shop_name}</title>
<style>
@media only screen and (max-width: 300px) {
body {
width: 218px !important;
margin: auto !important;
}
.table {
width: 195px !important;
margin: auto !important;
}
.logo,
.titleblock,
.linkbelow,
.box,
.footer,
.space_footer {
width: auto !important;
display: block !important;
}
span.title {
font-size: 20px !important;
line-height: 23px !important
}
span.subtitle {
font-size: 14px !important;
line-height: 18px !important;
padding-top: 10px !important;
display: block !important;
}
}
@media only screen and (min-width: 301px) and (max-width: 500px) {
body {
width: 308px !important;
margin: auto !important;
}
.table {
width: 285px !important;
margin: auto !important;
}
.logo,
.titleblock,
.linkbelow,
.box,
.footer,
.space_footer {
width: auto !important;
display: block !important;
}
}
@media only screen and (min-width: 501px) and (max-width: 768px) {
body {
width: 478px !important;
margin: auto !important;
}
.table {
width: 450px !important;
margin: auto !important;
}
.logo,
.titleblock,
.linkbelow,
.box,
.footer,
.space_footer {
width: auto !important;
display: block !important;
}
}
@media only screen and (max-device-width: 480px) {
body {
width: 308px !important;
margin: auto !important;
}
.table {
width: 285px;
margin: auto !important;
}
.logo,
.titleblock,
.linkbelow,
.box,
.footer,
.space_footer {
width: auto !important;
display: block !important;
}
}
</style>
</head>
<body style="-webkit-text-size-adjust:none;background-color:#fff;width:650px;font-family:Open-sans, sans-serif;color:#555454;font-size:13px;line-height:18px;margin:auto">
<table class="table table-mail" style="width:100%;margin-top:10px;-moz-box-shadow:0 0 5px #afafaf;-webkit-box-shadow:0 0 5px #afafaf;-o-box-shadow:0 0 5px #afafaf;box-shadow:0 0 5px #afafaf;filter:progid:DXImageTransform.Microsoft.Shadow(color=#afafaf,Direction=134,Strength=5)">
<tr>
<td class="space" style="width:20px;padding:7px 0">&nbsp;</td>
<td align="center" style="padding:7px 0">
<table class="table" bgcolor="#ffffff" style="width:100%">
<tr>
<td align="center" class="logo" style="border-bottom:4px solid #333333;padding:7px 0">
<a title="{shop_name}" href="{shop_url}" target="_blank" style="color:#337ff1">
<img src="{shop_logo}" alt="{shop_name}" />
</a>
</td>
</tr>
<tr>
<td align="center" class="titleblock" style="padding:7px 0">
<font size="2" face="Open-sans, sans-serif" color="#555454">
<span class="title" style="font-weight:500;font-size:28px;text-transform:uppercase;line-height:33px">
Action needed
</span>
<br />
<span class="subtitle" style="font-weight:500;font-size:16px;text-transform:uppercase;line-height:25px">
Google Hotel Center could not verify your hotel
</span>
</font>
</td>
</tr>
<tr>
<td class="space_footer" style="padding:0!important">&nbsp;</td>
</tr>
<tr>
<td class="box" style="border:1px solid #D6D4D4;background-color:#f8f8f8;padding:7px 0">
<table class="table" style="width:100%">
<tr>
<td width="10" style="padding:7px 0">&nbsp;</td>
<td style="padding:7px 0">
<font size="2" face="Open-sans, sans-serif" color="#555454">
<p data-html-only="1" style="border-bottom:1px solid #D6D4D4;margin:3px 0 7px;text-transform:uppercase;font-weight:500;font-size:18px;padding-bottom:10px">
Hi,
</p>
<span style="color:#777;">
Google was unable to verify your hotel <b>{hotel_name}</b> for Google Hotel Center.
</span>
<br />
<br />
<span>
<strong>Reason:</strong>
<span style="color:#777;">{failure_reason}</span>
</span>
<br />
<br />
<span style="color:#777;">
Please review and correct the issue above. The Google Hotel Connector will automatically resubmit this hotel the next time it syncs with Google.
</span>
</font>
</td>
<td width="10" style="padding:7px 0">&nbsp;</td>
</tr>
</table>
</td>
</tr>
<tr>
<td class="space_footer" style="padding:0!important">&nbsp;</td>
</tr>
<tr>
<td class="footer" style="border-top:4px solid #333333;padding:7px 0">
<span>
<a href="{shop_url}" target="_blank" style="color:#337ff1"> {shop_name} </a> powered by <a href="https://webkul.com" target="_blank" style="color:#337ff1">Webkul&trade;</a>
</span>
</td>
</tr>
</table>
</td>
<td class="space" style="width:20px;padding:7px 0">&nbsp;</td>
</tr>
</table>
</body>
</html>

View File

@@ -0,0 +1,16 @@
[/{shop_url}]
Action needed
Google Hotel Center could not verify your hotel
Hi,
Google was unable to verify your hotel {hotel_name} for Google Hotel Center.
Reason: {failure_reason}
Please review and correct the issue above. The Google Hotel Connector will automatically resubmit this hotel the next time it syncs with Google.
{shop_name} [/{shop_url}] powered by Webkul(tm)
[https://webkul.com]

View File

@@ -0,0 +1,199 @@
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/1999/REC-html401-19991224/strict.dtd">
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=0" />
<title>Message from {shop_name}</title>
<style>
@media only screen and (max-width: 300px) {
body {
width: 218px !important;
margin: auto !important;
}
.table {
width: 195px !important;
margin: auto !important;
}
.logo,
.titleblock,
.linkbelow,
.box,
.footer,
.space_footer {
width: auto !important;
display: block !important;
}
span.title {
font-size: 20px !important;
line-height: 23px !important
}
span.subtitle {
font-size: 14px !important;
line-height: 18px !important;
padding-top: 10px !important;
display: block !important;
}
}
@media only screen and (min-width: 301px) and (max-width: 500px) {
body {
width: 308px !important;
margin: auto !important;
}
.table {
width: 285px !important;
margin: auto !important;
}
.logo,
.titleblock,
.linkbelow,
.box,
.footer,
.space_footer {
width: auto !important;
display: block !important;
}
}
@media only screen and (min-width: 501px) and (max-width: 768px) {
body {
width: 478px !important;
margin: auto !important;
}
.table {
width: 450px !important;
margin: auto !important;
}
.logo,
.titleblock,
.linkbelow,
.box,
.footer,
.space_footer {
width: auto !important;
display: block !important;
}
}
@media only screen and (max-device-width: 480px) {
body {
width: 308px !important;
margin: auto !important;
}
.table {
width: 285px;
margin: auto !important;
}
.logo,
.titleblock,
.linkbelow,
.box,
.footer,
.space_footer {
width: auto !important;
display: block !important;
}
}
</style>
</head>
<body style="-webkit-text-size-adjust:none;background-color:#fff;width:650px;font-family:Open-sans, sans-serif;color:#555454;font-size:13px;line-height:18px;margin:auto">
<table class="table table-mail" style="width:100%;margin-top:10px;-moz-box-shadow:0 0 5px #afafaf;-webkit-box-shadow:0 0 5px #afafaf;-o-box-shadow:0 0 5px #afafaf;box-shadow:0 0 5px #afafaf;filter:progid:DXImageTransform.Microsoft.Shadow(color=#afafaf,Direction=134,Strength=5)">
<tr>
<td class="space" style="width:20px;padding:7px 0">&nbsp;</td>
<td align="center" style="padding:7px 0">
<table class="table" bgcolor="#ffffff" style="width:100%">
<tr>
<td align="center" class="logo" style="border-bottom:4px solid #333333;padding:7px 0">
<a title="{shop_name}" href="{shop_url}" target="_blank" style="color:#337ff1">
<img src="{shop_logo}" alt="{shop_name}" />
</a>
</td>
</tr>
<tr>
<td align="center" class="titleblock" style="padding:7px 0">
<font size="2" face="Open-sans, sans-serif" color="#555454">
<span class="title" style="font-weight:500;font-size:28px;text-transform:uppercase;line-height:33px">
Great news!
</span>
<br />
<span class="subtitle" style="font-weight:500;font-size:16px;text-transform:uppercase;line-height:25px">
Your hotel is now live on Google Hotel Center
</span>
</font>
</td>
</tr>
<tr>
<td class="space_footer" style="padding:0!important">&nbsp;</td>
</tr>
<tr>
<td class="box" style="border:1px solid #D6D4D4;background-color:#f8f8f8;padding:7px 0">
<table class="table" style="width:100%">
<tr>
<td width="10" style="padding:7px 0">&nbsp;</td>
<td style="padding:7px 0">
<font size="2" face="Open-sans, sans-serif" color="#555454">
<p data-html-only="1" style="border-bottom:1px solid #D6D4D4;margin:3px 0 7px;text-transform:uppercase;font-weight:500;font-size:18px;padding-bottom:10px">
Hi,
</p>
<span style="color:#777;">
Your hotel <b>{hotel_name}</b> has been successfully verified and is now live on Google Hotel Center.
</span>
<br />
<br />
<span style="color:#777;">
Travelers can now discover the property on Google and access the available booking options.
</span>
<br />
<br />
<span style="color:#777;">
The Google Hotel Connector will continue to synchronize the hotel's relevant information, rates, availability, and other supported data with Google based on the configured connection.
</span>
</font>
</td>
<td width="10" style="padding:7px 0">&nbsp;</td>
</tr>
</table>
</td>
</tr>
<tr>
<td class="space_footer" style="padding:0!important">&nbsp;</td>
</tr>
<tr>
<td align="center" class="linkbelow" style="padding:15px 0">
<a href="{hotel_url}" target="_blank" style="background-color:#337ff1;color:#ffffff;text-decoration:none;padding:10px 24px;border-radius:4px;font-weight:500;display:inline-block;">
View Your Hotel Page
</a>
</td>
</tr>
<tr>
<td class="space_footer" style="padding:0!important">&nbsp;</td>
</tr>
<tr>
<td class="footer" style="border-top:4px solid #333333;padding:7px 0">
<span>
<a href="{shop_url}" target="_blank" style="color:#337ff1"> {shop_name} </a> powered by <a href="https://webkul.com" target="_blank" style="color:#337ff1">Webkul&trade;</a>
</span>
</td>
</tr>
</table>
</td>
<td class="space" style="width:20px;padding:7px 0">&nbsp;</td>
</tr>
</table>
</body>
</html>

View File

@@ -0,0 +1,18 @@
[/{shop_url}]
Great news!
Your hotel is now live on Google Hotel Center
Hi,
Your hotel {hotel_name} has been successfully verified and is now live on Google Hotel Center.
Travelers can now discover the property on Google and access the available booking options.
The Google Hotel Connector will continue to synchronize the hotel's relevant information, rates, availability, and other supported data with Google based on the configured connection.
View Your Hotel Page [/{hotel_url}]
{shop_name} [/{shop_url}] powered by Webkul(tm)
[https://webkul.com]

View File

@@ -0,0 +1,32 @@
<?php
/**
* 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
*/
header("Expires: Mon, 26 Jul 1997 05:00:00 GMT");
header("Last-Modified: ".gmdate("D, d M Y H:i:s")." GMT");
header("Cache-Control: no-store, no-cache, must-revalidate");
header("Cache-Control: post-check=0, pre-check=0", false);
header("Pragma: no-cache");
header("Location: ../");
exit;

View File

@@ -0,0 +1,32 @@
<?php
/**
* 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
*/
header("Expires: Mon, 26 Jul 1997 05:00:00 GMT");
header("Last-Modified: ".gmdate("D, d M Y H:i:s")." GMT");
header("Cache-Control: no-store, no-cache, must-revalidate");
header("Cache-Control: post-check=0, pre-check=0", false);
header("Pragma: no-cache");
header("Location: ../");
exit;

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,32 @@
<?php
/**
* 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
*/
header('Expires: Mon, 26 Jul 1997 05:00:00 GMT');
header('Last-Modified: '.gmdate('D, d M Y H:i:s').' GMT');
header('Cache-Control: no-store, no-cache, must-revalidate');
header('Cache-Control: post-check=0, pre-check=0', false);
header('Pragma: no-cache');
header('Location: ../../../../../');
exit;

View File

@@ -0,0 +1,165 @@
/**
* 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
*/
/* ── Table spacing & vertical alignment ───────────────────────── */
.qlo-ghc-table th,
.qlo-ghc-table td {
padding: 12px 16px !important;
vertical-align: middle !important;
}
/* ── Dashboard notification widget (left-align overrides RTL-safe dashboards) */
#ghc-dashboard-notification,
#ghc-dashboard-notification h3,
#ghc-dashboard-notification p,
#ghc-dashboard-notification ul,
#ghc-dashboard-notification li,
#ghc-dashboard-notification .btn {
text-align: left;
}
/* ── Prevent scrollbar jitter on GHC admin pages ─────────────────
Scoped to exactly the controllers this stylesheet is loaded on
(see hookActionAdminControllerSetMedia) via QloApps admin's own
body.{controller} class, instead of a bare `html` selector that
would silently apply to any future page this CSS gets added to. */
body.admingooglehotelconfig,
body.admingooglehotellogs,
body.adminaddhotel,
body.admindashboard {
overflow-y: scroll;
}
/* ── Admin list — shrink actions column to content width ────────── */
table.qghc_api_log th.actions,
table.qghc_api_log thead tr.nodrag th:last-child {
width: 1px;
white-space: nowrap;
}
table.qghc_api_log th.actions .pull-right {
display: flex;
flex-direction: column;
align-items: flex-end;
gap: 4px;
}
/* ── Log detail view — copy-to-clipboard block ─────────────────── */
.qlo-ghc-log-block {
position: relative;
}
.qlo-ghc-copy-btn {
position: absolute;
top: 8px;
right: 8px;
z-index: 1;
opacity: 0.75;
}
.qlo-ghc-copy-btn:hover {
opacity: 1;
}
.qlo-ghc-log-pre {
background: #f8f8f8;
border: 1px solid #e1e1e1;
border-radius: 4px;
padding: 12px 14px;
padding-top: 36px;
max-height: 420px;
overflow: auto;
font-size: 12px;
line-height: 1.6;
white-space: pre-wrap;
word-break: break-all;
font-family: 'Courier New', Courier, monospace;
}
/* ── Dashboard notification widget ─────────────────────────────── */
.qlo-ghc-notif-title { margin-bottom: 6px; }
.qlo-ghc-notif-desc { font-size: 12px; margin-bottom: 8px; }
.qlo-ghc-notif-list { margin-bottom: 10px; }
.qlo-ghc-notif-item { margin-bottom: 4px; }
.qlo-ghc-badge-urgent { font-size: 10px; vertical-align: middle; }
.qlo-ghc-notif-dot { font-size: 8px; vertical-align: middle; }
/* ── Hotel tab content ─────────────────────────────────────────── */
.qlo-ghc-alert-margin { margin: 16px; }
.qlo-ghc-alert-offset { margin: 0 15px 15px; }
.qlo-ghc-alert-mb { margin-bottom: 16px; }
.qlo-ghc-col-padded { padding-top: 6px; }
.qlo-ghc-status-label { font-size: 12px; padding: 5px 10px; }
.qlo-ghc-missing-list { margin: 8px 0 0 0; padding-left: 20px; }
.qlo-ghc-switch-disabled { opacity: 0.5; cursor: not-allowed; }
/* Double class to out-specify core's ".bootstrap p { margin: ... }" without !important. */
.qlo-ghc-helpblock-top.qlo-ghc-helpblock-top { margin-top: 10px; }
.qlo-ghc-label-normal { font-weight: normal; margin: 0; }
.qlo-ghc-hidden { display: none; }
/* ── Config page ───────────────────────────────────────────────── */
.qlo-ghc-flash-wrap { margin-bottom: 16px; }
.qlo-ghc-panel-heading-flex {
display: -webkit-box;
display: -ms-flexbox;
display: flex;
-webkit-box-align: center;
-ms-flex-align: center;
align-items: center;
-webkit-box-pack: justify;
-ms-flex-pack: justify;
justify-content: space-between;
-ms-flex-wrap: wrap;
flex-wrap: wrap;
gap: 6px;
}
.qlo-ghc-heading-text { line-height: 1.4; }
.qlo-ghc-conn-badge { font-size: 11px; padding: 4px 8px; white-space: nowrap; }
.qlo-ghc-panel-footer-flex {
display: -webkit-box;
display: -ms-flexbox;
display: flex;
-webkit-box-align: center;
-ms-flex-align: center;
align-items: center;
-webkit-box-pack: justify;
-ms-flex-pack: justify;
justify-content: space-between;
}
.qlo-ghc-no-margin { margin: 0; }
.qlo-ghc-panel-body-no-pt { padding-top: 0; }
.qlo-ghc-no-mb { margin-bottom: 0; }
.qlo-ghc-nowrap { white-space: nowrap; }
/* ── Left-align form labels — PS admin theme defaults .control-label to
right-aligned. Scoped only to the standalone GHC pages (Config, Logs) —
NOT #hotel-tab, since that pane sits inside the shared Add/Edit Hotel form
alongside native tabs (Information, SEO, ...) that all use the standard
right-aligned label style; overriding it there made GHC's tab look
inconsistent with its own siblings instead of matching them. */
.qlo-ghc-form-left-labels .control-label {
text-align: left;
}

View File

@@ -0,0 +1,26 @@
/**
* 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
*/
.icon-AdminGoogleHotelParent::before {
content: '\f236';
font-family: 'FontAwesome'
}

View File

@@ -0,0 +1,32 @@
<?php
/**
* 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
*/
header('Expires: Mon, 26 Jul 1997 05:00:00 GMT');
header('Last-Modified: '.gmdate('D, d M Y H:i:s').' GMT');
header('Cache-Control: no-store, no-cache, must-revalidate');
header('Cache-Control: post-check=0, pre-check=0', false);
header('Pragma: no-cache');
header('Location: ../../../../');
exit;

View File

@@ -0,0 +1,32 @@
<?php
/**
* 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
*/
header('Expires: Mon, 26 Jul 1997 05:00:00 GMT');
header('Last-Modified: '.gmdate('D, d M Y H:i:s').' GMT');
header('Cache-Control: no-store, no-cache, must-revalidate');
header('Cache-Control: post-check=0, pre-check=0', false);
header('Pragma: no-cache');
header('Location: ../../../');
exit;

View File

@@ -0,0 +1,32 @@
<?php
/**
* 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
*/
header('Expires: Mon, 26 Jul 1997 05:00:00 GMT');
header('Last-Modified: '.gmdate('D, d M Y H:i:s').' GMT');
header('Cache-Control: no-store, no-cache, must-revalidate');
header('Cache-Control: post-check=0, pre-check=0', false);
header('Pragma: no-cache');
header('Location: ../../../../../');
exit;

View File

@@ -0,0 +1,182 @@
/**
* 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
*/
(function ($) {
'use strict';
// Exposed globally — called via onclick attribute on room-type checkboxes
window.checkAllRoomTypes = function (checked) {
$('.qlo-ghc-room-type-cb').prop('checked', checked);
if (checked) {
$('#room-type-error').remove();
}
};
$(document).ready(function () {
// ── Hotel edit page — move "not connected" warning above the panel ──
var $topWarning = $('#ghc-hotel-top-warning');
if ($topWarning.length && $('#ajax_confirmation').length) {
$topWarning.insertBefore('#ajax_confirmation');
}
// ── Log detail view — clipboard copy ────────────────────────────
$(document).on('click', '.qlo-ghc-copy-btn', function () {
var $btn = $(this);
var targetId = $btn.attr('data-target');
var $pre = $('#' + targetId);
if (!$pre.length) {
return;
}
var text = $pre[0].innerText || $pre[0].textContent;
if (navigator.clipboard && window.isSecureContext) {
navigator.clipboard.writeText(text).then(function () {
showCopied($btn);
});
} else {
var $ta = $('<textarea>').val(text).css({ position: 'fixed', opacity: 0 }).appendTo('body');
$ta[0].select();
document.execCommand('copy');
$ta.remove();
showCopied($btn);
}
});
// ── Hotel edit page — Google Hotel tab ──────────────────────────
$('input[name="hotel_status"]').on('change', function () {
if ('0' === $(this).val()) {
var msg = (typeof ghcDisableHotelMsg !== 'undefined' && ghcDisableHotelMsg)
? ghcDisableHotelMsg
: 'Are you sure you want to disable this hotel from Google Hotels?';
if (!confirm(msg)) {
$('input#hotel_status_on').prop('checked', true);
return;
}
$('.qlo-ghc-room-type-cb, #room_types_all').prop('checked', false);
$('#room-types-section').addClass('qlo-ghc-hidden');
} else {
// Auto-select all rooms when GHC is first enabled
$('.qlo-ghc-room-type-cb, #room_types_all').prop('checked', true);
$('#room-types-section').removeClass('qlo-ghc-hidden');
}
});
// Sync "select all" checkbox state with individual room-type rows
$(document).on('change', '.qlo-ghc-room-type-cb', function () {
var total = $('.qlo-ghc-room-type-cb').length;
var checked = $('.qlo-ghc-room-type-cb:checked').length;
$('#room_types_all').prop('checked', total === checked);
if (checked > 0) {
$('#room-type-error').remove();
}
});
// ── Hotel Information tab — normalize lat/long precision before submit ──
// Google's map picker can return coordinates with 15+ decimal digits (float
// arithmetic artifacts, e.g. 28.629770299999997 — real precision to only ~7
// decimals, the rest is noise). PrestaShop's own Validate::isFloat() rejects
// those — it checks strval((float)$v) == strval($v), which fails once a value
// has more significant digits than PHP's default float-to-string precision
// reproduces — so the hotel save is blocked with "Property ...->latitude is not
// valid". Rounding here to 7 decimals (far more precision than a hotel location
// needs, and within the DB's decimal(*,8) columns) and letting parseFloat() strip
// trailing zeros produces a clean string that always round-trips. Applies to
// whichever field is present — the map's hidden inputs when maps are enabled, or
// the manual fallback inputs when not.
var $latLngForm = $('#loclatitude, #loclongitude').closest('form');
if ($latLngForm.length) {
$latLngForm.on('submit', function () {
['#loclatitude', '#loclongitude'].forEach(function (selector) {
var $field = $(selector);
var val = $field.val();
if (val !== '' && !isNaN(val)) {
$field.val(parseFloat(parseFloat(val).toFixed(7)).toString());
}
});
});
}
// ── Hotel form — client-side GHC validation ──────────────────────
// hookActionBeforeAddHotelValidation is non-standard — may not exist on all
// QloApps builds. JS validation is the portable, module-only alternative.
var $statusInput = $('input[name="hotel_status"]');
if ($statusInput.length) {
$statusInput.closest('form').on('submit', function (e) {
var isEnabled = $('input[name="hotel_status"][value="1"]').prop('checked');
if (!isEnabled) {
return true;
}
if (0 === $('.qlo-ghc-room-type-cb:checked').length) {
e.preventDefault();
showRoomTypeError();
return false;
}
$('#room-type-error').remove();
return true;
});
}
});
// Mirrors alerts.tpl's own alert-danger markup (dismissible, inserted at the top
// of the page above #ajax_confirmation) so this reads like every other QloApps
// admin error instead of an ad-hoc box next to the field.
function showRoomTypeError() {
$('#room-type-error').remove();
// This check is client-side only, so blocking submit here never triggers a new
// page load — a confirmation banner left over from an earlier successful save
// (alerts.tpl's .bootstrap > .alert-success) would otherwise keep showing
// alongside this new error, which a real server-side error page never would.
// #ajax_confirmation itself is excluded — it's the anchor #ajax_confirmation
// that the new error box is inserted before, a few lines down, and on this
// page it can end up matching this same selector, which silently breaks that
// insertion (jQuery's .before() on an empty selection is a no-op).
$('.bootstrap .alert-success').not('#ajax_confirmation').remove();
var msg = (typeof noRoomTypeError !== 'undefined' && noRoomTypeError)
? noRoomTypeError
: 'Please select at least one room type before enabling Google Hotel.';
var $alert = $('<div>', { class: 'alert alert-danger' }).text(msg);
$('<button>', { type: 'button', class: 'close', 'data-dismiss': 'alert' }).html('&times;').prependTo($alert);
var $err = $('<div>', { id: 'room-type-error', class: 'bootstrap' }).append($alert);
$('#ajax_confirmation').before($err);
// Switch to Google Hotel tab so the admin can see and fix the checkboxes
var $tabLink = $('a[href="#hotel-tab"]');
if ($tabLink.length) {
$tabLink.tab('show');
}
$('html, body').animate({ scrollTop: 0 }, 300);
}
function showCopied($btn) {
var orig = $btn.html();
$btn.html('<i class="icon-check"></i> Copied!').addClass('btn-success').removeClass('btn-default');
setTimeout(function () {
$btn.html(orig).removeClass('btn-success').addClass('btn-default');
}, 2000);
}
}(jQuery));

View File

@@ -0,0 +1,32 @@
<?php
/**
* 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
*/
header('Expires: Mon, 26 Jul 1997 05:00:00 GMT');
header('Last-Modified: '.gmdate('D, d M Y H:i:s').' GMT');
header('Cache-Control: no-store, no-cache, must-revalidate');
header('Cache-Control: post-check=0, pre-check=0', false);
header('Pragma: no-cache');
header('Location: ../../../../');
exit;

View File

@@ -0,0 +1,215 @@
{*
* 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
*}
{* ── Google Hotel Setup ───────────────────────────────────────────────── *}
<div class="panel qlo-ghc-form-left-labels">
<div class="panel-heading qlo-ghc-panel-heading-flex">
<span class="qlo-ghc-heading-text">
<i class="icon-link"></i>
{l s='Google Hotel Setup' mod='qlogooglehotelconnector'}
</span>
{if $is_connected}
<span class="label label-success qlo-ghc-conn-badge">
<i class="icon-check"></i>&nbsp;{l s='Connected' mod='qlogooglehotelconnector'}
</span>
{else}
<span class="label label-danger qlo-ghc-conn-badge">
<i class="icon-times"></i>&nbsp;{l s='Not Connected' mod='qlogooglehotelconnector'}
</span>
{/if}
</div>
{* Connection form — fields only, no footer here *}
<form id="ghc-connection-form"
method="post"
action="{$action_url|escape:'html':'UTF-8'}"
class="form-horizontal">
<input type="hidden" name="submitConfigurationConnection" value="1">
<div class="panel-body">
<div class="alert alert-warning qlo-ghc-alert-mb">
<strong>{l s='Important:' mod='qlogooglehotelconnector'}</strong>
{l s='This key was automatically generated for the Google Hotel connection. Please do not change the permissions of this webservice key.' mod='qlogooglehotelconnector'}
{if !$is_connected}
<br><br>
{l s='To complete the setup, click' mod='qlogooglehotelconnector'}
<strong>{l s='Create Connection' mod='qlogooglehotelconnector'}</strong>.
{/if}
</div>
<div class="form-group">
<label class="control-label col-lg-3" for="QGHC_WS_KEY">
{l s='Webservice Key' mod='qlogooglehotelconnector'}
</label>
<div class="col-lg-7">
<input type="text"
class="form-control"
id="QGHC_WS_KEY"
name="QGHC_WS_KEY"
value="{$ws_key|escape:'html':'UTF-8'}"
placeholder="{l s='Enter your webservice key' mod='qlogooglehotelconnector'}" />
{if $ws_key}
<p class="qlo-ghc-helpblock-top">
<button type="submit" form="ghc-regenerate-form" class="btn btn-default btn-sm">
<i class="icon-refresh"></i>
{l s='Regenerate Key' mod='qlogooglehotelconnector'}
</button>
</p>
{/if}
</div>
</div>
</div>
</form>
{* Delete form — empty, button lives in the shared footer below *}
{if $is_connected}
<form id="ghc-delete-form"
method="post"
action="{$action_url|escape:'html':'UTF-8'}"
onsubmit="return confirm('{l s='Are you sure you want to delete the Google Hotel connection?' mod='qlogooglehotelconnector' js=1}')">
<input type="hidden" name="submitDeleteConfigurationConnection" value="1">
</form>
{/if}
{* Regenerate form — empty, button lives under the Webservice Key field above *}
{if $ws_key}
<form id="ghc-regenerate-form"
method="post"
action="{$action_url|escape:'html':'UTF-8'}"
onsubmit="return confirm('{l s='Regenerating the key will invalidate the current webservice key immediately. Continue?' mod='qlogooglehotelconnector' js=1}')">
<input type="hidden" name="submitRegenerateWebserviceKey" value="1">
</form>
{/if}
{* Single shared footer — both buttons sit here *}
<div class="panel-footer qlo-ghc-panel-footer-flex">
{if $is_connected}
<button type="submit" form="ghc-delete-form" class="btn btn-default">
<i class="process-icon-delete"></i>
{l s='Delete Connection' mod='qlogooglehotelconnector'}
</button>
{else}
<span></span>
{/if}
<button type="submit" form="ghc-connection-form" class="btn btn-default">
<i class="process-icon-save"></i>&nbsp;
{if $is_connected}
{l s='Update Connection' mod='qlogooglehotelconnector'}
{else}
{l s='Create Connection' mod='qlogooglehotelconnector'}
{/if}
</button>
</div>
</div>
{* ── Cron job setting ─────────────────────────────────────────────────── *}
<div class="panel">
<div class="panel-heading">
<i class="icon-clock-o"></i>
{l s='Cron job setting' mod='qlogooglehotelconnector'}
</div>
<div class="panel-body">
<div class="alert alert-info qlo-ghc-no-margin">
<strong>
{l s='For automatic cleanup of API logs older than 2 months, set the following cron job to run once every 24 hours:' mod='qlogooglehotelconnector'}
</strong>
<br><br>
<strong>
<code>0 0 * * * curl {$cron_url|escape:'html':'UTF-8'}</code>
</strong>
</div>
</div>
</div>
{* ── Connected Properties ───────────────────────────────────────────────── *}
<div class="panel">
<div class="panel-heading">
<i class="icon-globe"></i>
{l s='Google Hotel — Connected Properties' mod='qlogooglehotelconnector'}
{if $configs}
&nbsp;<span class="badge">{$configs|@count}</span>
{/if}
</div>
<div class="panel-body">
<p class="text-muted qlo-ghc-no-margin">
{l s='Hotels listed here are enabled for Google Hotel. To add or remove a hotel, go to' mod='qlogooglehotelconnector'}
<strong>
<a href="{$hotel_list_url|escape:'html':'UTF-8'}">{l s='Hotels' mod='qlogooglehotelconnector'}</a>
{l s='→ Edit Hotel → Google Hotel tab' mod='qlogooglehotelconnector'}
</strong>.
</p>
</div>
{if !$configs}
<div class="panel-body qlo-ghc-panel-body-no-pt">
<div class="alert alert-info qlo-ghc-no-margin">
<i class="icon-info-circle"></i>
{l s='No hotels are connected to Google Hotel yet. Open any hotel from the Hotels section and enable it in the Google Hotel tab.' mod='qlogooglehotelconnector'}
</div>
</div>
{else}
<div class="table-responsive">
<table class="table table-striped qlo-ghc-table qlo-ghc-no-mb">
<thead>
<tr>
<th>{l s='Hotel' mod='qlogooglehotelconnector'}</th>
<th>{l s='Google Status' mod='qlogooglehotelconnector'}</th>
<th>{l s='Last Updated' mod='qlogooglehotelconnector'}</th>
</tr>
</thead>
<tbody>
{foreach from=$configs item=cfg}
<tr>
<td>
<a href="{$cfg.edit_url|escape:'html':'UTF-8'}">
{$cfg.hotel_name|escape:'html':'UTF-8'}
</a>
</td>
<td>
<span class="label {$cfg.status_class|escape:'html':'UTF-8'}">
<i class="{$cfg.status_icon|escape:'html':'UTF-8'}"></i>
{$cfg.status_label|escape:'html':'UTF-8'}
</span>
</td>
<td class="qlo-ghc-nowrap">
{$cfg.date_add|escape:'html':'UTF-8'}
</td>
</tr>
{/foreach}
</tbody>
</table>
</div>
{/if}
</div>

View File

@@ -0,0 +1,22 @@
{*
* 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
*}
<span class="badge {$badge|escape:'html':'UTF-8'}">{$text|escape:'html':'UTF-8'}</span>

View File

@@ -0,0 +1,32 @@
<?php
/**
* 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
*/
header('Expires: Mon, 26 Jul 1997 05:00:00 GMT');
header('Last-Modified: '.gmdate('D, d M Y H:i:s').' GMT');
header('Cache-Control: no-store, no-cache, must-revalidate');
header('Cache-Control: post-check=0, pre-check=0', false);
header('Pragma: no-cache');
header('Location: ../../../../../');
exit;

View File

@@ -0,0 +1,133 @@
{*
* 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
*}
<div class="panel qlo-ghc-form-left-labels">
<div class="panel-heading">
<i class="icon-file-text"></i>
{l s='API Log Detail' mod='qlogooglehotelconnector'}
&nbsp;&mdash;&nbsp;
<span class="text-muted">{l s='Log #' mod='qlogooglehotelconnector'}{$ghc_log.id_qghc_api_log|intval}</span>
</div>
<div class="panel-body">
<div class="form-horizontal">
<div class="form-group">
<label class="control-label col-lg-3 col-md-4">
{l s='Date' mod='qlogooglehotelconnector'}
</label>
<div class="col-lg-9 col-md-8">
<p class="form-control-static">{$ghc_log.date_add|escape:'html':'UTF-8'}</p>
</div>
</div>
<div class="form-group">
<label class="control-label col-lg-3 col-md-4">
{l s='Hotel' mod='qlogooglehotelconnector'}
</label>
<div class="col-lg-9 col-md-8">
<p class="form-control-static">{$ghc_log.hotel_label|escape:'html':'UTF-8'}</p>
</div>
</div>
<div class="form-group">
<label class="control-label col-lg-3 col-md-4">
{l s='Log Type' mod='qlogooglehotelconnector'}
</label>
<div class="col-lg-9 col-md-8">
<p class="form-control-static">{$ghc_log.type_label|escape:'html':'UTF-8'}</p>
</div>
</div>
<div class="form-group">
<label class="control-label col-lg-3 col-md-4">
{l s='Status' mod='qlogooglehotelconnector'}
</label>
<div class="col-lg-9 col-md-8">
<p class="form-control-static">
{if $ghc_log.status}
<span class="label label-success">{l s='Success' mod='qlogooglehotelconnector'}</span>
{else}
<span class="label label-danger">{l s='Error' mod='qlogooglehotelconnector'}</span>
{/if}
</p>
</div>
</div>
{if $ghc_log.message}
<div class="form-group">
<label class="control-label col-lg-3 col-md-4">
{l s='Message' mod='qlogooglehotelconnector'}
</label>
<div class="col-lg-9 col-md-8">
<p class="form-control-static">{$ghc_log.message|escape:'html':'UTF-8'}</p>
</div>
</div>
{/if}
<div class="form-group">
<label class="control-label col-lg-3 col-md-4">
{l s='Request' mod='qlogooglehotelconnector'}
</label>
<div class="col-lg-9 col-md-8">
{if $ghc_log.request}
<div class="qlo-ghc-log-block">
<button type="button" class="btn btn-default btn-xs qlo-ghc-copy-btn" data-target="ghc-request">
<i class="icon-copy"></i> {l s='Copy' mod='qlogooglehotelconnector'}
</button>
<pre class="qlo-ghc-log-pre" id="ghc-request">{$ghc_log.request|escape:'html':'UTF-8'}</pre>
</div>
{else}
<p class="form-control-static text-muted">{l s='(empty)' mod='qlogooglehotelconnector'}</p>
{/if}
</div>
</div>
<div class="form-group">
<label class="control-label col-lg-3 col-md-4">
{l s='Response' mod='qlogooglehotelconnector'}
</label>
<div class="col-lg-9 col-md-8">
{if $ghc_log.response}
<div class="qlo-ghc-log-block">
<button type="button" class="btn btn-default btn-xs qlo-ghc-copy-btn" data-target="ghc-response">
<i class="icon-copy"></i> {l s='Copy' mod='qlogooglehotelconnector'}
</button>
<pre class="qlo-ghc-log-pre" id="ghc-response">{$ghc_log.response|escape:'html':'UTF-8'}</pre>
</div>
{else}
<p class="form-control-static text-muted">{l s='(empty)' mod='qlogooglehotelconnector'}</p>
{/if}
</div>
</div>
</div>
</div>
<div class="panel-footer">
<a href="{$back_url|escape:'html':'UTF-8'}" class="btn btn-default">
<i class="process-icon-back"></i>
{l s='Back to Logs' mod='qlogooglehotelconnector'}
</a>
</div>
</div>

View File

@@ -0,0 +1,23 @@
{*
* 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
*}
{* PS admin list renderer output — already HTML, must not be escaped *}
{$content}

View File

@@ -0,0 +1,27 @@
{*
* 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
*}
<li>
<a href="#hotel-tab" data-toggle="tab">
<i class="icon-globe"></i>
{l s='Google Hotel' mod='qlogooglehotelconnector'}
</a>
</li>

View File

@@ -0,0 +1,213 @@
{*
* 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
*}
<div class="tab-pane" id="hotel-tab">
{if !$has_ws_key}
<div class="alert alert-warning qlo-ghc-alert-margin">
{l s='Google Hotel is not configured yet. Please set up the webservice key before enabling hotels.' mod='qlogooglehotelconnector'}
&nbsp;<a href="{$config_url|escape:'html':'UTF-8'}" class="alert-link">
{l s='Go to Google Hotel Connector configuration' mod='qlogooglehotelconnector'}
<i class="icon-external-link"></i>
</a>
</div>
{elseif $id_hotel}
{* ── Weekly review notice — shown until google_status is active ─ *}
{if !isset($property.id) || !$property.id || $property.google_status != $google_status_connected}
<div class="alert alert-info qlo-ghc-alert-offset">
{l s='Google reviews newly activated properties on a weekly basis. Once you enable the Google Channel for a hotel, it may take up to 1 week for the property to be processed and become available on Google. Once approved, the status will be automatically updated here.' mod='qlogooglehotelconnector'}
</div>
{/if}
{* ── Google Status label ─────────────────────────────────────── *}
<div class="form-group">
<label class="control-label col-sm-3">
<span>{l s='Google Status' mod='qlogooglehotelconnector'}</span>
</label>
<div class="col-sm-6 qlo-ghc-col-padded">
{if isset($property.id) && $property.id && $property.google_status == $google_status_connected}
<span class="label label-success qlo-ghc-status-label">
<i class="icon-check"></i>&nbsp;{l s='Connected' mod='qlogooglehotelconnector'}
</span>
{elseif isset($property.id) && $property.id && $property.google_status == $google_status_pending}
<span class="label label-warning qlo-ghc-status-label">
<i class="icon-clock-o"></i>&nbsp;{l s='Pending' mod='qlogooglehotelconnector'}
</span>
{else}
<span class="label label-default qlo-ghc-status-label">
<i class="icon-times-circle"></i>&nbsp;{l s='Not Connected' mod='qlogooglehotelconnector'}
</span>
{/if}
</div>
</div>
{* ── Missing fields warning ──────────────────────────────────── *}
{if !empty($missing_fields)}
<div class="alert alert-warning qlo-ghc-alert-offset">
<strong>{l s='The following required fields are missing. Please fill them in before enabling Google Hotel:' mod='qlogooglehotelconnector'}
</strong>
<ul class="qlo-ghc-missing-list">
{foreach $missing_fields as $field}
<li>{$field|escape:'html':'UTF-8'}</li>
{/foreach}
</ul>
</div>
{/if}
{* ── Hotel Enable / Disable ───────────────────────────────────── *}
<div class="form-group">
<label class="control-label col-sm-3">
<span>{l s='Enable on Google Hotel' mod='qlogooglehotelconnector'}</span>
</label>
<div class="col-sm-6">
{if !empty($missing_fields)}
{* Required fields are missing: lock the toggle to Off *}
<input type="hidden" name="hotel_status" value="0">
<span class="switch prestashop-switch fixed-width-lg qlo-ghc-switch-disabled">
<input type="radio" value="1" id="hotel_status_on" disabled="disabled">
<label for="hotel_status_on">{l s='Yes' mod='qlogooglehotelconnector'}</label>
<input type="radio" value="0" id="hotel_status_off" disabled="disabled" checked="checked">
<label for="hotel_status_off">{l s='No' mod='qlogooglehotelconnector'}</label>
<a class="slide-button btn"></a>
</span>
<p class="help-block qlo-ghc-helpblock-top">
{l s='Fill in the missing fields above to enable Google Hotel.' mod='qlogooglehotelconnector'}
</p>
{else}
<span class="switch prestashop-switch fixed-width-lg">
<input type="radio"
value="1"
id="hotel_status_on"
name="hotel_status"
{if $is_post && isset($smarty.post.hotel_status) && $smarty.post.hotel_status == '1'}
checked="checked"
{elseif !$is_post && isset($property.id) && $property.id}
checked="checked"
{/if}>
<label for="hotel_status_on">{l s='Yes' mod='qlogooglehotelconnector'}</label>
<input type="radio"
value="0"
id="hotel_status_off"
name="hotel_status"
{if $is_post && isset($smarty.post.hotel_status) && $smarty.post.hotel_status == '0'}
checked="checked"
{elseif !$is_post && (!isset($property.id) || !$property.id)}
checked="checked"
{/if}>
<label for="hotel_status_off">{l s='No' mod='qlogooglehotelconnector'}</label>
<a class="slide-button btn"></a>
</span>
{/if}
</div>
</div>
{* ── Room Types (shown when hotel is enabled and all required fields are set) *}
{assign var=status_enabled value=false}
{if empty($missing_fields)}
{if $is_post && isset($smarty.post.hotel_status) && $smarty.post.hotel_status == '1'}
{assign var=status_enabled value=true}
{elseif !$is_post && isset($property.id) && $property.id}
{assign var=status_enabled value=true}
{/if}
{/if}
<div class="form-group{if !$status_enabled} qlo-ghc-hidden{/if}" id="room-types-section">
<label class="control-label col-sm-3">
<span>{l s='Room Types' mod='qlogooglehotelconnector'}</span>
</label>
<div class="col-sm-6">
{if $room_types}
<div class="row">
<div class="col-sm-12">
<table class="table table-bordered">
<thead>
<tr>
<th class="fixed-width-xs">
<input type="checkbox"
id="room_types_all"
onclick="checkAllRoomTypes(this.checked)" />
</th>
<th>
{l s='Room Type' mod='qlogooglehotelconnector'}
</th>
</tr>
</thead>
<tbody>
{foreach $room_types as $room_type}
{assign var=cb_id value='room_type_'|cat:$room_type.id_product}
<tr>
<td>
<input type="checkbox"
id="{$cb_id|escape:'html':'UTF-8'}"
name="room_types[]"
class="qlo-ghc-room-type-cb"
value="{$room_type.id_product|intval}"
{if $is_post && isset($checked_room_types[$room_type.id_product])}
checked="checked"
{elseif !$is_post && isset($room_type_mappings[$room_type.id_product])}
checked="checked"
{/if} />
</td>
<td>
<label for="{$cb_id|escape:'html':'UTF-8'}" class="qlo-ghc-label-normal">
{$room_type.room_type|escape:'html':'UTF-8'}
</label>
</td>
</tr>
{/foreach}
</tbody>
</table>
</div>
</div>
{else}
<p class="help-block">
{l s='No room types found for this hotel.' mod='qlogooglehotelconnector'}
</p>
{/if}
</div>
</div>
{else}
{* New hotel — show only the enable toggle; room types are available after first save *}
<div class="alert alert-info qlo-ghc-alert-offset">
{l s='Save the hotel first. You will be able to select room types when editing this hotel.' mod='qlogooglehotelconnector'}
</div>
<div class="form-group">
<label class="control-label col-sm-3">
<span>{l s='Enable on Google Hotel' mod='qlogooglehotelconnector'}</span>
</label>
<div class="col-sm-6">
<span class="switch prestashop-switch fixed-width-lg">
<input type="radio" value="1" id="hotel_status_on" name="hotel_status"
{if $is_post && isset($smarty.post.hotel_status) && $smarty.post.hotel_status == '1'}checked="checked"{/if}>
<label for="hotel_status_on">{l s='Yes' mod='qlogooglehotelconnector'}</label>
<input type="radio" value="0" id="hotel_status_off" name="hotel_status"
{if !$is_post || !isset($smarty.post.hotel_status) || $smarty.post.hotel_status == '0'}checked="checked"{/if}>
<label for="hotel_status_off">{l s='No' mod='qlogooglehotelconnector'}</label>
<a class="slide-button btn"></a>
</span>
</div>
</div>
{/if}
</div>

View File

@@ -0,0 +1,67 @@
{*
* 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
*}
<div class="col-sm-12">
<section id="ghc-dashboard-notification" class="widget panel">
<h3>
<i class="icon-{if $is_connected && empty($hotel_names)}check-circle text-success{else}warning-sign text-warning{/if}"></i>
{l s='Google Hotel Connector' mod='qlogooglehotelconnector'}
</h3>
{if !$is_connected}
<p class="qlo-ghc-notif-desc">
{l s='Not connected to the Google Hotel Center.' mod='qlogooglehotelconnector'}
&nbsp;<a href="{$config_url|escape:'html':'UTF-8'}">{l s='Fix in Google Hotel Setup' mod='qlogooglehotelconnector'}</a>
</p>
{elseif !empty($hotel_names)}
<p class="qlo-ghc-notif-desc">
{l s='Connected to the Google Hotel Center, but some hotels are not listed.' mod='qlogooglehotelconnector'}
</p>
{else}
<p class="qlo-ghc-notif-desc">
{l s='Connected to the Google Hotel Center.' mod='qlogooglehotelconnector'}
</p>
{/if}
{if !empty($hotel_names)}
<p class="qlo-ghc-notif-title">
<strong>{l s='Hotels with an issue:' mod='qlogooglehotelconnector'}</strong>
&nbsp;<span class="label label-danger qlo-ghc-badge-urgent">{l s='Urgent Action Needed' mod='qlogooglehotelconnector'}</span>
</p>
<ul class="list-unstyled qlo-ghc-notif-list">
{foreach from=$hotel_names item=hotelName}
<li class="qlo-ghc-notif-item">
<i class="icon-circle text-danger qlo-ghc-notif-dot"></i>
&nbsp;<strong>{$hotelName|escape:'html':'UTF-8'}</strong>
</li>
{/foreach}
</ul>
<p class="qlo-ghc-notif-desc">
<a href="{$hotel_list_url|escape:'html':'UTF-8'}">{l s='View Hotel List' mod='qlogooglehotelconnector'}</a>
</p>
{/if}
<a href="{$config_url|escape:'html':'UTF-8'}" class="btn btn-default btn-sm btn-block">
<i class="icon-cog"></i>
{l s='Google Hotel Setup' mod='qlogooglehotelconnector'}
</a>
</section>
</div>

View File

@@ -0,0 +1,44 @@
{*
* 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
*}
<div class="row">
<div class="col-lg-12">
{if !$is_connected}
<div class="alert alert-danger">
<strong>{l s='Not connected to the Google Hotel Center.' mod='qlogooglehotelconnector'}</strong>
{l s='Hotels cannot sync with Google Hotel Ads until this is fixed.' mod='qlogooglehotelconnector'}
&nbsp;<a href="{$config_url|escape:'html':'UTF-8'}" class="alert-link">{l s='Fix in Google Hotel Setup' mod='qlogooglehotelconnector'}</a>
</div>
{/if}
{if !empty($hotel_names)}
<div class="alert alert-warning">
<strong>{l s='The following hotels are not listed on Google Hotel:' mod='qlogooglehotelconnector'}</strong>
<ul class="qlo-ghc-missing-list">
{foreach from=$hotel_names item=hotelName}
<li>{$hotelName|escape:'html':'UTF-8'}</li>
{/foreach}
</ul>
<a href="{$hotel_list_url|escape:'html':'UTF-8'}" class="alert-link">{l s='View Hotel List' mod='qlogooglehotelconnector'}</a>
</div>
{/if}
</div>
</div>

View File

@@ -0,0 +1,41 @@
{*
* 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
*}
<div id="ghc-hotel-top-warning">
{if !$is_connected}
<div class="alert alert-danger qlo-ghc-alert-mb">
<strong>{l s='Not connected to the Google Hotel Center.' mod='qlogooglehotelconnector'}</strong>
{l s='Hotels cannot sync with Google Hotel Ads until this is fixed.' mod='qlogooglehotelconnector'}
&nbsp;<a href="{$hotel_edit_config_url|escape:'html':'UTF-8'}" class="alert-link">{l s='Fix in Google Hotel Setup' mod='qlogooglehotelconnector'}</a>
</div>
{/if}
{if !empty($issue_reasons)}
<div class="alert alert-warning">
<strong>{l s='This hotel is not listed on Google Hotel:' mod='qlogooglehotelconnector'}</strong>
<ul class="qlo-ghc-missing-list">
{foreach from=$issue_reasons item=reason}
<li>{$reason|escape:'html':'UTF-8'} {l s='Use the Google Hotel tab below to enable it.' mod='qlogooglehotelconnector'}</li>
{/foreach}
</ul>
</div>
{/if}
</div>

View File

@@ -0,0 +1,39 @@
{*
* 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
*}
{if !$is_connected}
<div class="alert alert-danger">
<strong>{l s='Not connected to the Google Hotel Center.' mod='qlogooglehotelconnector'}</strong>
{l s='Hotels cannot sync with Google Hotel Ads until this is fixed.' mod='qlogooglehotelconnector'}
&nbsp;<a href="{$config_url|escape:'html':'UTF-8'}" class="alert-link">{l s='Fix in Google Hotel Setup' mod='qlogooglehotelconnector'}</a>
</div>
{/if}
{if !empty($hotels_with_issues)}
<div class="alert alert-warning">
<strong>{l s='The following hotels are not listed on Google Hotel:' mod='qlogooglehotelconnector'}</strong>
<ul class="qlo-ghc-missing-list">
{foreach from=$hotels_with_issues item=hotel}
<li><a href="{$hotel.edit_url|escape:'html':'UTF-8'}">{$hotel.hotel_name|escape:'html':'UTF-8'}</a></li>
{/foreach}
</ul>
</div>
{/if}

View File

@@ -0,0 +1,32 @@
<?php
/**
* 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
*/
header('Expires: Mon, 26 Jul 1997 05:00:00 GMT');
header('Last-Modified: '.gmdate('D, d M Y H:i:s').' GMT');
header('Cache-Control: no-store, no-cache, must-revalidate');
header('Cache-Control: post-check=0, pre-check=0', false);
header('Pragma: no-cache');
header('Location: ../../../../../');
exit;

View File

@@ -0,0 +1,32 @@
<?php
/**
* 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
*/
header('Expires: Mon, 26 Jul 1997 05:00:00 GMT');
header('Last-Modified: '.gmdate('D, d M Y H:i:s').' GMT');
header('Cache-Control: no-store, no-cache, must-revalidate');
header('Cache-Control: post-check=0, pre-check=0', false);
header('Pragma: no-cache');
header('Location: ../../../../');
exit;