hotel_beds 0.2.1 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/INTERNALS.md +6 -0
- data/README.md +55 -29
- data/lib/hotel_beds/client.rb +11 -3
- data/lib/hotel_beds/connection.rb +2 -3
- data/lib/hotel_beds/hotel_basket_add/envelope.rb +71 -0
- data/lib/hotel_beds/hotel_basket_add/operation.rb +36 -0
- data/lib/hotel_beds/hotel_basket_add/request.rb +49 -0
- data/lib/hotel_beds/hotel_basket_add/response.rb +66 -0
- data/lib/hotel_beds/hotel_search/envelope.rb +27 -18
- data/lib/hotel_beds/hotel_search/parser/errors.rb +49 -0
- data/lib/hotel_beds/hotel_search/parser/hotel.rb +67 -0
- data/lib/hotel_beds/hotel_search/parser/price.rb +38 -0
- data/lib/hotel_beds/hotel_search/parser/room.rb +46 -0
- data/lib/hotel_beds/hotel_search/parser/room_grouper.rb +101 -0
- data/lib/hotel_beds/hotel_search/request.rb +5 -23
- data/lib/hotel_beds/hotel_search/response.rb +16 -68
- data/lib/hotel_beds/model.rb +0 -1
- data/lib/hotel_beds/model/available_room.rb +9 -11
- data/lib/hotel_beds/model/cancellation_policy.rb +14 -0
- data/lib/hotel_beds/model/hotel.rb +4 -0
- data/lib/hotel_beds/model/purchase.rb +16 -0
- data/lib/hotel_beds/model/requested_room.rb +7 -0
- data/lib/hotel_beds/model/room.rb +31 -0
- data/lib/hotel_beds/model/search_result.rb +0 -5
- data/lib/hotel_beds/model/service.rb +18 -0
- data/lib/hotel_beds/version.rb +1 -1
- data/spec/features/adding_a_hotel_to_basket_spec.rb +58 -0
- data/spec/features/hotel_search_spec.rb +31 -9
- data/spec/lib/hotel_beds/hotel_search/parser/room_grouper_spec.rb +53 -0
- data/spec/spec_helper.rb +8 -0
- metadata +20 -5
- data/lib/hotel_beds/hotel_search/room_grouper.rb +0 -64
- data/spec/lib/hotel_beds/hotel_search/room_grouper_spec.rb +0 -45
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA1:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: b442fa8e1dd84060190628131cdc1411cd170780
|
|
4
|
+
data.tar.gz: 3f0e1420814abb2208119060056f66f813ef32e2
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 873348c0c1b44e5218efd5f0f784cf6c235ea9e61327f312b40e474925e99a04d7406fcf882ecafa447d4bed2742fc4687a4247293824898c763608eb2f9bd48
|
|
7
|
+
data.tar.gz: 9405c30f24d1b2fd0e4428f70ed96205059ed97dd4b0f3143fa6d4036d7dd8f960c5e6a62da847621c17d5af6d251b138983bbbcc2adda9dfaef2bb69a7e71b2
|
data/INTERNALS.md
CHANGED
|
@@ -7,3 +7,9 @@ The process we use to group results is as follows:
|
|
|
7
7
|
3) Group the count of each requested room (RC) by occupants (OK)
|
|
8
8
|
4) For each group, determine all combinations of the available rooms (AR) for the given count (RC), this gives us the search results per room group (RSRG)
|
|
9
9
|
5) Find all combinations of the search results per room group, giving us unique search results (SR)
|
|
10
|
+
|
|
11
|
+
# Basket
|
|
12
|
+
|
|
13
|
+
HotelBeds effectively has a basket system (known as the "ServiceAdd" API), as it covers more than just hotel rooms.
|
|
14
|
+
|
|
15
|
+
The "contract remarks" or terms and conditions it returns must be presented to the customer before the basket is checked-out.
|
data/README.md
CHANGED
|
@@ -16,24 +16,48 @@ Manually, via command line:
|
|
|
16
16
|
|
|
17
17
|
## Usage
|
|
18
18
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
19
|
+
```ruby
|
|
20
|
+
# create the connection to HotelBeds
|
|
21
|
+
client = HotelBeds::Client.new(endpoint: :test, username: "user", password: "pass")
|
|
22
|
+
|
|
23
|
+
# perform the search
|
|
24
|
+
search = client.perform_hotel_search({
|
|
25
|
+
check_in_date: Date.today,
|
|
26
|
+
check_out_date: Date.today + 1,
|
|
27
|
+
rooms: [{ adult_count: 2 }],
|
|
28
|
+
destination: "SYD"
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
# inspect the response
|
|
32
|
+
puts search.response.hotels
|
|
33
|
+
# => [<HotelBeds::Model::Hotel>, <HotelBeds::Model::Hotel>]
|
|
34
|
+
puts search.response.total_pages
|
|
35
|
+
# => 10
|
|
36
|
+
puts search.response.current_page
|
|
37
|
+
# => 1
|
|
38
|
+
|
|
39
|
+
# place a booking
|
|
40
|
+
booking = client.perform_hotel_booking({
|
|
41
|
+
room_ids: [search.response.hotels.first.results.first.id],
|
|
42
|
+
people: [
|
|
43
|
+
{ title: "Mr", name: "David Smith", type: :adult },
|
|
44
|
+
{ title: "Mrs", name: "Jane Smith", type: :adult }
|
|
45
|
+
],
|
|
46
|
+
address: {
|
|
47
|
+
line_1: "123 Some Street",
|
|
48
|
+
city: "Townsville",
|
|
49
|
+
state: "New Statestown",
|
|
50
|
+
postcode: "NS1 1AB"
|
|
51
|
+
country: "UK"
|
|
52
|
+
},
|
|
53
|
+
phone_number: "+44 1234 567 890",
|
|
54
|
+
email: "david.smith@example.com"
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
# inspect the response
|
|
58
|
+
puts booking.response.reference
|
|
59
|
+
# => "ABC-123"
|
|
60
|
+
```
|
|
37
61
|
|
|
38
62
|
### Options
|
|
39
63
|
|
|
@@ -41,17 +65,19 @@ The HotelBeds API will return individual rooms, rather than being grouped by wha
|
|
|
41
65
|
|
|
42
66
|
Example:
|
|
43
67
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
68
|
+
```ruby
|
|
69
|
+
# perform the search
|
|
70
|
+
search = client.perform_hotel_search({
|
|
71
|
+
check_in_date: Date.today,
|
|
72
|
+
check_out_date: Date.today + 1,
|
|
73
|
+
rooms: [
|
|
74
|
+
{ adult_count: 2 },
|
|
75
|
+
{ adult_count: 1, child_count: 1, child_ages: [7] }
|
|
76
|
+
],
|
|
77
|
+
destination: "SYD",
|
|
78
|
+
group_results: true
|
|
79
|
+
})
|
|
80
|
+
```
|
|
55
81
|
|
|
56
82
|
## Contributing
|
|
57
83
|
|
data/lib/hotel_beds/client.rb
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
require "hotel_beds/configuration"
|
|
2
2
|
require "hotel_beds/connection"
|
|
3
3
|
require "hotel_beds/hotel_search/operation"
|
|
4
|
+
require "hotel_beds/hotel_basket_add/operation"
|
|
4
5
|
|
|
5
6
|
module HotelBeds
|
|
6
7
|
class Client
|
|
@@ -12,13 +13,20 @@ module HotelBeds
|
|
|
12
13
|
self.connection = Connection.new(configuration)
|
|
13
14
|
freeze
|
|
14
15
|
end
|
|
15
|
-
|
|
16
|
-
#
|
|
17
|
-
#
|
|
16
|
+
|
|
17
|
+
# each method returns an operation object which contains both the
|
|
18
|
+
# request and response objects.
|
|
19
|
+
|
|
18
20
|
def perform_hotel_search(*args)
|
|
19
21
|
HotelSearch::Operation.new(*args).perform(
|
|
20
22
|
connection: connection
|
|
21
23
|
)
|
|
22
24
|
end
|
|
25
|
+
|
|
26
|
+
def add_hotel_room_to_basket(*args)
|
|
27
|
+
HotelBasketAdd::Operation.new(*args).perform(
|
|
28
|
+
connection: connection
|
|
29
|
+
)
|
|
30
|
+
end
|
|
23
31
|
end
|
|
24
32
|
end
|
|
@@ -18,7 +18,6 @@ module HotelBeds
|
|
|
18
18
|
:"@xmlns:xsi" => "http://www.w3.org/2001/XMLSchema-instance",
|
|
19
19
|
:"@xsi:schemaLocation" => "http://www.hotelbeds.com/schemas/2005/06/messages #{namespace}.xsd",
|
|
20
20
|
:@echoToken => SecureRandom.hex[0..15],
|
|
21
|
-
:@sessionId => SecureRandom.hex[0..15],
|
|
22
21
|
:Credentials => {
|
|
23
22
|
User: configuration.username,
|
|
24
23
|
Password: configuration.password
|
|
@@ -34,7 +33,7 @@ module HotelBeds
|
|
|
34
33
|
message: message
|
|
35
34
|
})
|
|
36
35
|
end
|
|
37
|
-
|
|
36
|
+
|
|
38
37
|
private
|
|
39
38
|
def initialize_client
|
|
40
39
|
Savon::Client.new do |config|
|
|
@@ -70,4 +69,4 @@ module HotelBeds
|
|
|
70
69
|
end
|
|
71
70
|
end
|
|
72
71
|
end
|
|
73
|
-
end
|
|
72
|
+
end
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
require "delegate"
|
|
2
|
+
|
|
3
|
+
module HotelBeds
|
|
4
|
+
module HotelBasketAdd
|
|
5
|
+
class Envelope < SimpleDelegator
|
|
6
|
+
def attributes
|
|
7
|
+
{
|
|
8
|
+
Language: language,
|
|
9
|
+
Service: {
|
|
10
|
+
:"@xsi:type" => "ServiceHotel",
|
|
11
|
+
:"@availToken" => service.availability_token,
|
|
12
|
+
:ContractList => { Contract: {
|
|
13
|
+
Name: service.contract_name,
|
|
14
|
+
IncomingOffice: { :@code => service.contract_incoming_office_code }
|
|
15
|
+
} },
|
|
16
|
+
:DateFrom => { :@date => service.check_in_date.strftime("%Y%m%d") },
|
|
17
|
+
:DateTo => { :@date => service.check_out_date.strftime("%Y%m%d") },
|
|
18
|
+
:HotelInfo => {
|
|
19
|
+
Code: service.hotel_code,
|
|
20
|
+
Destination: {
|
|
21
|
+
:@code => service.destination_code,
|
|
22
|
+
:@type => "SIMPLE"
|
|
23
|
+
}
|
|
24
|
+
},
|
|
25
|
+
:AvailableRoom => available_rooms
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
private
|
|
31
|
+
def available_rooms
|
|
32
|
+
Array(service.rooms).group_by(&:group_key).values.map do |rooms|
|
|
33
|
+
{
|
|
34
|
+
HotelOccupancy: build_room(rooms),
|
|
35
|
+
HotelRoom: {
|
|
36
|
+
Board: {
|
|
37
|
+
:@code => rooms.first.board_code,
|
|
38
|
+
:@type => "SIMPLE"
|
|
39
|
+
},
|
|
40
|
+
RoomType: {
|
|
41
|
+
:@code => rooms.first.room_type_code,
|
|
42
|
+
:@characteristic => rooms.first.room_type_characteristic,
|
|
43
|
+
:@type => "SIMPLE"
|
|
44
|
+
},
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def build_room(rooms)
|
|
51
|
+
child_ages = rooms.map(&:child_ages).inject(Array.new, :+)
|
|
52
|
+
adult_count = rooms.map(&:adult_count).inject(0, :+)
|
|
53
|
+
child_count = rooms.map(&:child_count).inject(0, :+)
|
|
54
|
+
{
|
|
55
|
+
RoomCount: rooms.size,
|
|
56
|
+
Occupancy: {
|
|
57
|
+
AdultCount: adult_count,
|
|
58
|
+
ChildCount: child_count,
|
|
59
|
+
GuestList: {
|
|
60
|
+
Customer: (1..adult_count).map {
|
|
61
|
+
{ :@type => "AD" }
|
|
62
|
+
} + (1..child_count).map { |i|
|
|
63
|
+
{ :@type => "CH", :Age => Integer(child_ages[i - 1]) }
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
end
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
require_relative "request"
|
|
2
|
+
require_relative "response"
|
|
3
|
+
require_relative "envelope"
|
|
4
|
+
|
|
5
|
+
module HotelBeds
|
|
6
|
+
module HotelBasketAdd
|
|
7
|
+
class Operation
|
|
8
|
+
attr_accessor :request, :response, :errors
|
|
9
|
+
private :request=, :response=, :errors=
|
|
10
|
+
|
|
11
|
+
def initialize(*args)
|
|
12
|
+
self.request = Request.new(*args)
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def perform(connection:)
|
|
16
|
+
if request.valid?
|
|
17
|
+
self.response = Response.new(request, retrieve(connection))
|
|
18
|
+
self.errors = response.errors
|
|
19
|
+
else
|
|
20
|
+
self.errors = request.errors
|
|
21
|
+
end
|
|
22
|
+
freeze
|
|
23
|
+
self
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
private
|
|
27
|
+
def retrieve(connection)
|
|
28
|
+
connection.call({
|
|
29
|
+
method: :serviceAdd,
|
|
30
|
+
namespace: :ServiceAddRQ,
|
|
31
|
+
data: Envelope.new(request).attributes
|
|
32
|
+
})
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
require "securerandom"
|
|
2
|
+
require "hotel_beds/model"
|
|
3
|
+
require "hotel_beds/model/available_room"
|
|
4
|
+
|
|
5
|
+
module HotelBeds
|
|
6
|
+
module HotelBasketAdd
|
|
7
|
+
class Request
|
|
8
|
+
class HotelService
|
|
9
|
+
include HotelBeds::Model
|
|
10
|
+
|
|
11
|
+
# attributes
|
|
12
|
+
attribute :availability_token, String
|
|
13
|
+
attribute :contract_name, String
|
|
14
|
+
attribute :contract_incoming_office_code, String
|
|
15
|
+
attribute :check_in_date, Date
|
|
16
|
+
attribute :check_out_date, Date
|
|
17
|
+
attribute :hotel_code, String
|
|
18
|
+
attribute :destination_code, String
|
|
19
|
+
attribute :rooms, Array[HotelBeds::Model::AvailableRoom]
|
|
20
|
+
|
|
21
|
+
# validation
|
|
22
|
+
validates :destination_code, length: { is: 3 }
|
|
23
|
+
validates :rooms, length: { minimum: 1, maximum: 5 }
|
|
24
|
+
validates :session_id, :availability_token, :contract_name,
|
|
25
|
+
:contract_incoming_office_code, :check_in_date, :check_out_date,
|
|
26
|
+
:hotel_code, presence: true
|
|
27
|
+
validate do |service|
|
|
28
|
+
unless (1..5).cover?(service.rooms.size)
|
|
29
|
+
service.errors.add(:rooms, "quantity must be between 1 and 5")
|
|
30
|
+
end
|
|
31
|
+
unless service.rooms.all?(&:valid?)
|
|
32
|
+
service.errors.add(:rooms, "are invalid")
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
include HotelBeds::Model
|
|
38
|
+
|
|
39
|
+
# attributes
|
|
40
|
+
attribute :session_id, String, default: SecureRandom.hex[0..15]
|
|
41
|
+
attribute :language, String, default: "ENG"
|
|
42
|
+
attribute :service, HotelService
|
|
43
|
+
|
|
44
|
+
# validation
|
|
45
|
+
validates :language, length: { is: 3 }
|
|
46
|
+
validates :session_id, :service, presence: true
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
require "active_model/errors"
|
|
2
|
+
require "hotel_beds/model/purchase"
|
|
3
|
+
|
|
4
|
+
module HotelBeds
|
|
5
|
+
module HotelBasketAdd
|
|
6
|
+
class Response
|
|
7
|
+
attr_accessor :request, :headers, :body, :errors
|
|
8
|
+
private :request=, :headers=, :body=, :errors=
|
|
9
|
+
|
|
10
|
+
def initialize(request, response)
|
|
11
|
+
self.request = request
|
|
12
|
+
self.headers = response.header
|
|
13
|
+
self.body = Nokogiri::XML(response.body.fetch(:service_add))
|
|
14
|
+
self.errors = ActiveModel::Errors.new(self).tap do |errors|
|
|
15
|
+
if response.http_error?
|
|
16
|
+
errors.add(:base, "HTTP error")
|
|
17
|
+
elsif response.soap_fault?
|
|
18
|
+
errors.add(:base, "SOAP error")
|
|
19
|
+
elsif !response.success?
|
|
20
|
+
errors.add(:base, "Request failed")
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
body.css("ErrorList Error").each do |error|
|
|
24
|
+
errors.add(:base, [
|
|
25
|
+
(sm = error.at_css("Message")) && sm.content,
|
|
26
|
+
(dm = error.at_css("DetailedMessage")) && dm.content
|
|
27
|
+
].compact.join("\n"))
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
freeze
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def inspect
|
|
34
|
+
"<#{self.class.name} errors=#{errors.inspect} headers=#{headers.inspect} body=#{body.to_s}>"
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def session_id
|
|
38
|
+
request.session_id
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def success?
|
|
42
|
+
errors.empty?
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def purchase
|
|
46
|
+
purchase = body.at_css("Purchase")
|
|
47
|
+
HotelBeds::Model::Purchase.new({
|
|
48
|
+
id: purchase.attr("purchaseToken"),
|
|
49
|
+
currency: purchase.at_css("Currency").attr("code"),
|
|
50
|
+
amount: purchase.at_css("TotalPrice").content,
|
|
51
|
+
services: purchase.css("ServiceList Service").map { |service|
|
|
52
|
+
{
|
|
53
|
+
id: service.attr("SPUI"),
|
|
54
|
+
contract_name: service.at_css("Contract Name").content,
|
|
55
|
+
contract_incoming_office_code: service.at_css("Contract IncomingOffice").attr("code"),
|
|
56
|
+
check_in_date: Date.parse(service.at_css("DateFrom").attr("date")),
|
|
57
|
+
check_out_date: Date.parse(service.at_css("DateTo").attr("date")),
|
|
58
|
+
currency: service.at_css("Currency").attr("code"),
|
|
59
|
+
amount: service.at_css("TotalAmount").content
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
})
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
end
|
|
@@ -5,11 +5,12 @@ module HotelBeds
|
|
|
5
5
|
class Envelope < SimpleDelegator
|
|
6
6
|
def attributes
|
|
7
7
|
{
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
8
|
+
:@sessionId => session_id,
|
|
9
|
+
:PaginationData => pagination_data,
|
|
10
|
+
:Language => language,
|
|
11
|
+
:CheckInDate => check_in_date,
|
|
12
|
+
:CheckOutDate => check_out_date,
|
|
13
|
+
:OccupancyList => occupancy_list
|
|
13
14
|
}.merge(Hash(destination)).merge(Hash(hotels)).merge(Hash(extra_params))
|
|
14
15
|
end
|
|
15
16
|
|
|
@@ -63,20 +64,28 @@ module HotelBeds
|
|
|
63
64
|
end
|
|
64
65
|
|
|
65
66
|
def occupancy_list
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
67
|
+
grouped_rooms = Array(rooms).group_by(&:group_key).values
|
|
68
|
+
{ HotelOccupancy: grouped_rooms.map(&method(:build_room)) }
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def build_room(rooms)
|
|
72
|
+
child_ages = rooms.map(&:child_ages).inject(Array.new, :+)
|
|
73
|
+
adult_count = rooms.map(&:adult_count).inject(0, :+)
|
|
74
|
+
child_count = rooms.map(&:child_count).inject(0, :+)
|
|
75
|
+
{
|
|
76
|
+
RoomCount: rooms.size,
|
|
77
|
+
Occupancy: {
|
|
78
|
+
AdultCount: adult_count,
|
|
79
|
+
ChildCount: child_count,
|
|
80
|
+
GuestList: {
|
|
81
|
+
Customer: (1..adult_count).map {
|
|
82
|
+
{ :@type => "AD" }
|
|
83
|
+
} + (1..child_count).map { |i|
|
|
84
|
+
{ :@type => "CH", :Age => Integer(child_ages[i - 1]) }
|
|
85
|
+
}
|
|
86
|
+
}
|
|
78
87
|
}
|
|
79
|
-
}
|
|
88
|
+
}
|
|
80
89
|
end
|
|
81
90
|
end
|
|
82
91
|
end
|