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
@@ -0,0 +1,49 @@
|
|
1
|
+
require "active_model/errors"
|
2
|
+
|
3
|
+
module HotelBeds
|
4
|
+
module HotelSearch
|
5
|
+
module Parser
|
6
|
+
class Errors
|
7
|
+
def self.call(*args, &block)
|
8
|
+
new(*args, &block).call
|
9
|
+
end
|
10
|
+
|
11
|
+
attr_accessor :response, :body
|
12
|
+
private :response, :body
|
13
|
+
|
14
|
+
def initialize(response, body)
|
15
|
+
self.response = response
|
16
|
+
self.body = body
|
17
|
+
freeze
|
18
|
+
end
|
19
|
+
|
20
|
+
def call
|
21
|
+
ActiveModel::Errors.new(self).tap do |errors|
|
22
|
+
apply_response_errors(errors)
|
23
|
+
apply_body_errors(errors)
|
24
|
+
end.freeze
|
25
|
+
end
|
26
|
+
|
27
|
+
private
|
28
|
+
def apply_response_errors(errors)
|
29
|
+
if response.http_error?
|
30
|
+
errors.add(:base, "HTTP error")
|
31
|
+
elsif response.soap_fault?
|
32
|
+
errors.add(:base, "SOAP error")
|
33
|
+
elsif !response.success?
|
34
|
+
errors.add(:base, "Request failed")
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
def apply_body_errors(errors)
|
39
|
+
body.css("ErrorList Error").each do |error|
|
40
|
+
errors.add(:base, [
|
41
|
+
(sm = error.at_css("Message")) && sm.content,
|
42
|
+
(dm = error.at_css("DetailedMessage")) && dm.content
|
43
|
+
].compact.join("\n"))
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
@@ -0,0 +1,67 @@
|
|
1
|
+
require "ostruct"
|
2
|
+
require "hotel_beds/model/hotel"
|
3
|
+
require "hotel_beds/hotel_search/parser/room"
|
4
|
+
require "hotel_beds/hotel_search/parser/room_grouper"
|
5
|
+
|
6
|
+
module HotelBeds
|
7
|
+
module HotelSearch
|
8
|
+
module Parser
|
9
|
+
class Hotel
|
10
|
+
def self.call(*args, &block)
|
11
|
+
new(*args, &block).call
|
12
|
+
end
|
13
|
+
|
14
|
+
attr_accessor :hotel, :currency, :request
|
15
|
+
private :hotel=, :currency=, :request=
|
16
|
+
|
17
|
+
def initialize(hotel:, currency:, request:)
|
18
|
+
self.hotel = hotel
|
19
|
+
self.currency = currency
|
20
|
+
self.request = request
|
21
|
+
freeze
|
22
|
+
end
|
23
|
+
|
24
|
+
def call
|
25
|
+
HotelBeds::Model::Hotel.new({
|
26
|
+
availability_token: hotel.attr("availToken"),
|
27
|
+
id: hotel.at_css("HotelInfo Code").content,
|
28
|
+
name: hotel.at_css("HotelInfo Name").content,
|
29
|
+
images: hotel.css("HotelInfo ImageList Image Url").map(&:content),
|
30
|
+
latitude: hotel.at_css("HotelInfo Position").attr("latitude"),
|
31
|
+
longitude: hotel.at_css("HotelInfo Position").attr("longitude"),
|
32
|
+
results: results,
|
33
|
+
destination_code: hotel.at_css("HotelInfo Destination").attr("code"),
|
34
|
+
contract_name: hotel.at_css("ContractList Contract Name").content,
|
35
|
+
contract_incoming_office_code: hotel.at_css("ContractList Contract IncomingOffice").attr("code")
|
36
|
+
})
|
37
|
+
end
|
38
|
+
|
39
|
+
private
|
40
|
+
def rooms
|
41
|
+
hotel.css("AvailableRoom")
|
42
|
+
end
|
43
|
+
|
44
|
+
def results
|
45
|
+
group_rooms(parsed_available_rooms).map do |rooms|
|
46
|
+
{ rooms: Array(rooms), currency: currency }
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
def group_rooms(rooms)
|
51
|
+
if request.group_results?
|
52
|
+
grouper = HotelBeds::HotelSearch::Parser::RoomGrouper
|
53
|
+
grouper.new(request.rooms, rooms).results
|
54
|
+
else
|
55
|
+
rooms
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
def parsed_available_rooms
|
60
|
+
Array(rooms).map do |room|
|
61
|
+
HotelBeds::HotelSearch::Parser::Room.call(room)
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
@@ -0,0 +1,38 @@
|
|
1
|
+
module HotelBeds
|
2
|
+
module HotelSearch
|
3
|
+
module Parser
|
4
|
+
class Price
|
5
|
+
def self.call(*args, &block)
|
6
|
+
new(*args, &block).call
|
7
|
+
end
|
8
|
+
|
9
|
+
attr_accessor :price
|
10
|
+
private :price
|
11
|
+
|
12
|
+
def initialize(price)
|
13
|
+
self.price = price
|
14
|
+
freeze
|
15
|
+
end
|
16
|
+
|
17
|
+
def call
|
18
|
+
(from..to).to_a.inject(Hash.new) do |result, date|
|
19
|
+
result.merge!(date => amount)
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
private
|
24
|
+
def from
|
25
|
+
Date.parse(price.at_css("DateTimeFrom").attr("date"))
|
26
|
+
end
|
27
|
+
|
28
|
+
def to
|
29
|
+
Date.parse(price.at_css("DateTimeTo").attr("date"))
|
30
|
+
end
|
31
|
+
|
32
|
+
def amount
|
33
|
+
BigDecimal.new(price.at_css("Amount").content, 3)
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
@@ -0,0 +1,46 @@
|
|
1
|
+
require "hotel_beds/hotel_search/parser/price"
|
2
|
+
require "hotel_beds/model/available_room"
|
3
|
+
|
4
|
+
module HotelBeds
|
5
|
+
module HotelSearch
|
6
|
+
module Parser
|
7
|
+
class Room
|
8
|
+
def self.call(*args, &block)
|
9
|
+
new(*args, &block).call
|
10
|
+
end
|
11
|
+
|
12
|
+
attr_accessor :room
|
13
|
+
private :room=
|
14
|
+
|
15
|
+
def initialize(room)
|
16
|
+
self.room = room
|
17
|
+
freeze
|
18
|
+
end
|
19
|
+
|
20
|
+
def call
|
21
|
+
HotelBeds::Model::AvailableRoom.new({
|
22
|
+
id: room.at_css("HotelRoom").attr("SHRUI"),
|
23
|
+
room_count: room.at_css("HotelOccupancy RoomCount").content,
|
24
|
+
adult_count: room.at_css("HotelOccupancy AdultCount").content,
|
25
|
+
child_count: room.at_css("HotelOccupancy ChildCount").content,
|
26
|
+
number_available: room.at_css("HotelRoom").attr("availCount"),
|
27
|
+
description: room.at_css("HotelRoom RoomType").content,
|
28
|
+
board: room.at_css("HotelRoom Board").content,
|
29
|
+
board_code: room.at_css("HotelRoom Board").attr("code"),
|
30
|
+
room_type_code: room.at_css("HotelRoom RoomType").attr("code"),
|
31
|
+
room_type_characteristic: room.at_css("HotelRoom RoomType").attr("characteristic"),
|
32
|
+
price: ((room.at_css("HotelRoom") > "Price") > "Amount").first.content,
|
33
|
+
rates: parse_price_list(room.css("Price PriceList Price"))
|
34
|
+
})
|
35
|
+
end
|
36
|
+
|
37
|
+
private
|
38
|
+
def parse_price_list(prices)
|
39
|
+
Array(prices).map do |price|
|
40
|
+
HotelBeds::HotelSearch::Parser::Price.call(price)
|
41
|
+
end.inject(Hash.new, :merge)
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
@@ -0,0 +1,101 @@
|
|
1
|
+
# see INTERNALS.md for comment symbols
|
2
|
+
module HotelBeds
|
3
|
+
module HotelSearch
|
4
|
+
module Parser
|
5
|
+
class RoomGrouper < Struct.new(:requested_rooms, :response_rooms)
|
6
|
+
def results
|
7
|
+
unique_room_combinations
|
8
|
+
end
|
9
|
+
|
10
|
+
private
|
11
|
+
def unique_room_combinations
|
12
|
+
combinations = build_combinations(room_options_grouped_by_occupants)
|
13
|
+
unique_combinations(expand_combinations(combinations))
|
14
|
+
end
|
15
|
+
|
16
|
+
def unique_combinations(combinations)
|
17
|
+
combinations.uniq { |r| r.sort_by(&:id).map(&:id) }
|
18
|
+
end
|
19
|
+
|
20
|
+
def expand_combinations(combinations)
|
21
|
+
combinations.map do |rooms|
|
22
|
+
rooms.inject(Array.new) do |result, room|
|
23
|
+
# get an array of all the child ages
|
24
|
+
child_ages = requested_rooms_child_ages_by_occupants.fetch(occupant_key(room))
|
25
|
+
1.upto(room.room_count) do |i|
|
26
|
+
result << room.dup.tap do |r|
|
27
|
+
r.room_count = 1
|
28
|
+
r.child_ages = child_ages.pop
|
29
|
+
end
|
30
|
+
end
|
31
|
+
result
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
# returns an array of room combinations for all rooms
|
37
|
+
def build_combinations(combinations)
|
38
|
+
head, *rest = combinations
|
39
|
+
Array(head).product(*rest)
|
40
|
+
end
|
41
|
+
|
42
|
+
# returns a array of arrays, each contains available rooms for a given
|
43
|
+
# room occupancy
|
44
|
+
def room_options_grouped_by_occupants
|
45
|
+
requested_room_count_by_occupants.inject(Array.new) do |result, (key, count)|
|
46
|
+
result << response_rooms_by_occupants.fetch(key).select do |room|
|
47
|
+
room.room_count == count
|
48
|
+
end
|
49
|
+
end
|
50
|
+
rescue KeyError => e
|
51
|
+
Array.new
|
52
|
+
end
|
53
|
+
|
54
|
+
# returns a hash of OK => [Integer]
|
55
|
+
def requested_rooms_child_ages_by_occupants
|
56
|
+
requested_rooms.inject(Hash.new) do |result, room|
|
57
|
+
key = occupant_key(room)
|
58
|
+
result[key] ||= Array.new
|
59
|
+
result[key] += room.child_ages
|
60
|
+
result
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
# returns a hash of OK => [RR]
|
65
|
+
def requested_rooms_by_occupants
|
66
|
+
requested_rooms.inject(Hash.new) do |result, room|
|
67
|
+
key = occupant_key(room)
|
68
|
+
result[key] ||= Array.new
|
69
|
+
result[key] << room
|
70
|
+
result
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
# returns a hash of OK => 1 (count)
|
75
|
+
def requested_room_count_by_occupants
|
76
|
+
requested_rooms_by_occupants.inject(Hash.new) do |result, (key, rooms)|
|
77
|
+
result.merge(key => rooms.size)
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
# returns a hash of OK => [AvailableRoom, AvailableRoom, AvailableRoom]
|
82
|
+
def response_rooms_by_occupants
|
83
|
+
response_rooms.inject(Hash.new) do |result, room|
|
84
|
+
key = occupant_key(room)
|
85
|
+
result[key] ||= Array.new
|
86
|
+
result[key].push(room)
|
87
|
+
result
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
# returns an OK for a given room
|
92
|
+
def occupant_key(room)
|
93
|
+
{
|
94
|
+
adult_count: room.adult_count,
|
95
|
+
child_count: room.child_count
|
96
|
+
}
|
97
|
+
end
|
98
|
+
end
|
99
|
+
end
|
100
|
+
end
|
101
|
+
end
|
@@ -1,44 +1,26 @@
|
|
1
|
+
require "securerandom"
|
1
2
|
require "hotel_beds/model"
|
3
|
+
require "hotel_beds/model/requested_room"
|
2
4
|
|
3
5
|
module HotelBeds
|
4
6
|
module HotelSearch
|
5
7
|
class Request
|
6
|
-
class Room
|
7
|
-
include HotelBeds::Model
|
8
|
-
|
9
|
-
# attributes
|
10
|
-
attribute :adult_count, Integer, default: 0
|
11
|
-
attribute :child_count, Integer, default: 0
|
12
|
-
attribute :child_ages, Array[Integer], default: Array.new
|
13
|
-
|
14
|
-
# validation
|
15
|
-
validates :adult_count, :child_count, numericality: {
|
16
|
-
greater_than_or_equal_to: 0,
|
17
|
-
less_than_or_equal_to: 4,
|
18
|
-
only_integer: true,
|
19
|
-
}
|
20
|
-
validate do |room|
|
21
|
-
unless child_ages.compact.size == child_count
|
22
|
-
room.errors.add(:child_ages, "must match quantity of children")
|
23
|
-
end
|
24
|
-
end
|
25
|
-
end
|
26
|
-
|
27
8
|
include HotelBeds::Model
|
28
9
|
|
29
10
|
# attributes
|
11
|
+
attribute :session_id, String, default: SecureRandom.hex[0..15]
|
30
12
|
attribute :page_number, Integer, default: 1
|
31
13
|
attribute :language, String, default: "ENG"
|
32
14
|
attribute :check_in_date, Date
|
33
15
|
attribute :check_out_date, Date
|
34
16
|
attribute :destination, String
|
35
17
|
attribute :hotels, Array[Integer]
|
36
|
-
attribute :rooms, Array[
|
18
|
+
attribute :rooms, Array[HotelBeds::Model::RequestedRoom]
|
37
19
|
attribute :group_results, Virtus::Attribute::Boolean, default: false
|
38
20
|
|
39
21
|
# validation
|
40
22
|
validates :language, :destination, length: { is: 3 }
|
41
|
-
validates :check_in_date, :check_out_date, presence: true
|
23
|
+
validates :session_id, :check_in_date, :check_out_date, presence: true
|
42
24
|
validates :rooms, length: { minimum: 1, maximum: 5 }
|
43
25
|
validates :page_number, numericality: {
|
44
26
|
greater_than: 0, only_integer: true
|
@@ -1,7 +1,6 @@
|
|
1
|
-
require "active_model/errors"
|
2
1
|
require "hotel_beds/model/hotel"
|
3
|
-
require "hotel_beds/
|
4
|
-
|
2
|
+
require "hotel_beds/hotel_search/parser/errors"
|
3
|
+
require "hotel_beds/hotel_search/parser/hotel"
|
5
4
|
|
6
5
|
module HotelBeds
|
7
6
|
module HotelSearch
|
@@ -13,22 +12,7 @@ module HotelBeds
|
|
13
12
|
self.request = request
|
14
13
|
self.headers = response.header
|
15
14
|
self.body = Nokogiri::XML(response.body.fetch(:get_hotel_valued_avail))
|
16
|
-
self.errors =
|
17
|
-
if response.http_error?
|
18
|
-
errors.add(:base, "HTTP error")
|
19
|
-
elsif response.soap_fault?
|
20
|
-
errors.add(:base, "SOAP error")
|
21
|
-
elsif !response.success?
|
22
|
-
errors.add(:base, "Request failed")
|
23
|
-
end
|
24
|
-
|
25
|
-
body.css("ErrorList Error").each do |error|
|
26
|
-
errors.add(:base, [
|
27
|
-
(sm = error.at_css("Message")) && sm.content,
|
28
|
-
(dm = error.at_css("DetailedMessage")) && dm.content
|
29
|
-
].compact.join("\n"))
|
30
|
-
end
|
31
|
-
end
|
15
|
+
self.errors = HotelBeds::HotelSearch::Parser::Errors.call(response, body)
|
32
16
|
freeze
|
33
17
|
end
|
34
18
|
|
@@ -36,6 +20,10 @@ module HotelBeds
|
|
36
20
|
"<#{self.class.name} errors=#{errors.inspect} headers=#{headers.inspect} body=#{body.to_s}>"
|
37
21
|
end
|
38
22
|
|
23
|
+
def session_id
|
24
|
+
request.session_id
|
25
|
+
end
|
26
|
+
|
39
27
|
def current_page
|
40
28
|
if data = pagination_data
|
41
29
|
Integer(data.attr("currentPage"))
|
@@ -52,16 +40,17 @@ module HotelBeds
|
|
52
40
|
end
|
53
41
|
end
|
54
42
|
|
43
|
+
def currency
|
44
|
+
body.at_css("Currency").attr("code")
|
45
|
+
end
|
46
|
+
|
55
47
|
def hotels
|
56
48
|
body.css("ServiceHotel").lazy.map do |hotel|
|
57
|
-
HotelBeds::
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
longitude: hotel.at_css("HotelInfo Position").attr("longitude"),
|
63
|
-
results: generate_results(hotel.css("AvailableRoom"))
|
64
|
-
})
|
49
|
+
HotelBeds::HotelSearch::Parser::Hotel.call(
|
50
|
+
hotel: hotel,
|
51
|
+
currency: currency,
|
52
|
+
request: request
|
53
|
+
)
|
65
54
|
end
|
66
55
|
end
|
67
56
|
|
@@ -69,47 +58,6 @@ module HotelBeds
|
|
69
58
|
def pagination_data
|
70
59
|
body.at_css("PaginationData")
|
71
60
|
end
|
72
|
-
|
73
|
-
def generate_results(rooms)
|
74
|
-
parsed_rooms = parse_available_rooms(rooms)
|
75
|
-
if request.group_results?
|
76
|
-
parsed_rooms = RoomGrouper.new(request.rooms, parsed_rooms).results
|
77
|
-
end
|
78
|
-
parsed_rooms.map do |rooms|
|
79
|
-
HotelBeds::Model::SearchResult.new({
|
80
|
-
rooms: Array(rooms),
|
81
|
-
currency: body.at_css("Currency").attr("code")
|
82
|
-
})
|
83
|
-
end
|
84
|
-
end
|
85
|
-
|
86
|
-
def parse_available_rooms(rooms)
|
87
|
-
Array(rooms).map do |room|
|
88
|
-
HotelBeds::Model::AvailableRoom.new({
|
89
|
-
id: room.at_css("HotelRoom").attr("SHRUI"),
|
90
|
-
adult_count: room.at_css("HotelOccupancy AdultCount").content,
|
91
|
-
child_count: room.at_css("HotelOccupancy ChildCount").content,
|
92
|
-
number_available: room.at_css("HotelRoom").attr("availCount"),
|
93
|
-
description: room.at_css("HotelRoom RoomType").content,
|
94
|
-
board: room.at_css("HotelRoom Board").content,
|
95
|
-
price: ((room.at_css("HotelRoom") > "Price") > "Amount").first.content,
|
96
|
-
rates: parse_price_list(room.css("Price PriceList Price"))
|
97
|
-
})
|
98
|
-
end
|
99
|
-
end
|
100
|
-
|
101
|
-
def parse_price_list(prices)
|
102
|
-
Array(prices).inject({}) do |result, price|
|
103
|
-
from = Date.parse(price.at_css("DateTimeFrom").attr("date"))
|
104
|
-
to = Date.parse(price.at_css("DateTimeTo").attr("date"))
|
105
|
-
dates = (from..to).to_a
|
106
|
-
amount = BigDecimal.new(price.at_css("Amount").content, 3)
|
107
|
-
dates.each do |date|
|
108
|
-
result.merge!(date => amount)
|
109
|
-
end
|
110
|
-
result
|
111
|
-
end
|
112
|
-
end
|
113
61
|
end
|
114
62
|
end
|
115
63
|
end
|