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.
Files changed (34) hide show
  1. checksums.yaml +4 -4
  2. data/INTERNALS.md +6 -0
  3. data/README.md +55 -29
  4. data/lib/hotel_beds/client.rb +11 -3
  5. data/lib/hotel_beds/connection.rb +2 -3
  6. data/lib/hotel_beds/hotel_basket_add/envelope.rb +71 -0
  7. data/lib/hotel_beds/hotel_basket_add/operation.rb +36 -0
  8. data/lib/hotel_beds/hotel_basket_add/request.rb +49 -0
  9. data/lib/hotel_beds/hotel_basket_add/response.rb +66 -0
  10. data/lib/hotel_beds/hotel_search/envelope.rb +27 -18
  11. data/lib/hotel_beds/hotel_search/parser/errors.rb +49 -0
  12. data/lib/hotel_beds/hotel_search/parser/hotel.rb +67 -0
  13. data/lib/hotel_beds/hotel_search/parser/price.rb +38 -0
  14. data/lib/hotel_beds/hotel_search/parser/room.rb +46 -0
  15. data/lib/hotel_beds/hotel_search/parser/room_grouper.rb +101 -0
  16. data/lib/hotel_beds/hotel_search/request.rb +5 -23
  17. data/lib/hotel_beds/hotel_search/response.rb +16 -68
  18. data/lib/hotel_beds/model.rb +0 -1
  19. data/lib/hotel_beds/model/available_room.rb +9 -11
  20. data/lib/hotel_beds/model/cancellation_policy.rb +14 -0
  21. data/lib/hotel_beds/model/hotel.rb +4 -0
  22. data/lib/hotel_beds/model/purchase.rb +16 -0
  23. data/lib/hotel_beds/model/requested_room.rb +7 -0
  24. data/lib/hotel_beds/model/room.rb +31 -0
  25. data/lib/hotel_beds/model/search_result.rb +0 -5
  26. data/lib/hotel_beds/model/service.rb +18 -0
  27. data/lib/hotel_beds/version.rb +1 -1
  28. data/spec/features/adding_a_hotel_to_basket_spec.rb +58 -0
  29. data/spec/features/hotel_search_spec.rb +31 -9
  30. data/spec/lib/hotel_beds/hotel_search/parser/room_grouper_spec.rb +53 -0
  31. data/spec/spec_helper.rb +8 -0
  32. metadata +20 -5
  33. data/lib/hotel_beds/hotel_search/room_grouper.rb +0 -64
  34. 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: 62d1e73612f9067924e5217e9a6ac519114c3c0f
4
- data.tar.gz: 3d73a53b48fc47773dc9d11b54f1d2d97862586c
3
+ metadata.gz: b442fa8e1dd84060190628131cdc1411cd170780
4
+ data.tar.gz: 3f0e1420814abb2208119060056f66f813ef32e2
5
5
  SHA512:
6
- metadata.gz: fcd299beabb15ae42f27b1398fb88db2d4901ab80a06987b35182d0fc494421377b7d8abf3d6d496627c0af469e98994c32c91ba79c5a8e89ad1d239caa311c9
7
- data.tar.gz: 6613c409737fc9b5b75d946a7681e1ee5555bef3a9422d2e5e128d9c77945db776e89c93abbb72d61bbd2ce9d7de4e6c2ecaeb947b7785eec97dea84959b6a6c
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
- # create the connection to HotelBeds
20
- client = HotelBeds::Client.new(endpoint: :test, username: "user", password: "pass")
21
-
22
- # perform the search
23
- search = client.perform_hotel_search({
24
- check_in_date: Date.today,
25
- check_out_date: Date.today + 1,
26
- rooms: [{ adult_count: 2 }],
27
- destination: "SYD"
28
- })
29
-
30
- # inspect the response
31
- puts search.response.hotels
32
- # => [<HotelBeds::Model::Hotel>, <HotelBeds::Model::Hotel>]
33
- puts search.response.total_pages
34
- # => 10
35
- puts search.response.current_page
36
- # => 1
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
- # perform the search
45
- search = client.perform_hotel_search({
46
- check_in_date: Date.today,
47
- check_out_date: Date.today + 1,
48
- rooms: [
49
- { adult_count: 2 },
50
- { adult_count: 1, child_count: 1, child_ages: [7] }
51
- ],
52
- destination: "SYD",
53
- group_results: true
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
 
@@ -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
- # builds and performs a hotel search, returning an operation object which
17
- # contains both the request and response objects.
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
- PaginationData: pagination_data,
9
- Language: language,
10
- CheckInDate: check_in_date,
11
- CheckOutDate: check_out_date,
12
- OccupancyList: occupancy_list
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
- { HotelOccupancy: Array(rooms).map { |room|
67
- guest_list = if room.child_count > 0
68
- { GuestList: { Customer: Array(room.child_ages).map { |age|
69
- { :@type => "CH", :Age => Integer(age) }
70
- } } }
71
- end
72
- {
73
- RoomCount: 1,
74
- Occupancy: (guest_list || Hash.new).merge({
75
- AdultCount: Integer(room.adult_count),
76
- ChildCount: Integer(room.child_count)
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