hotel_beds 0.2.1 → 0.3.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|