rthbound-suitcase 1.7.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (45) hide show
  1. data/.gitignore +4 -0
  2. data/CHANGELOG.md +17 -0
  3. data/Gemfile +3 -0
  4. data/README.md +83 -0
  5. data/Rakefile +13 -0
  6. data/examples/hash_adapter.rb +15 -0
  7. data/examples/hotel_image_db.rb +24 -0
  8. data/examples/redis_adapter.rb +29 -0
  9. data/lib/suitcase.rb +16 -0
  10. data/lib/suitcase/car_rental.rb +57 -0
  11. data/lib/suitcase/codes.rb +5 -0
  12. data/lib/suitcase/configuration.rb +29 -0
  13. data/lib/suitcase/core_ext/string.rb +13 -0
  14. data/lib/suitcase/hotel.rb +355 -0
  15. data/lib/suitcase/hotel/amenity.rb +46 -0
  16. data/lib/suitcase/hotel/bed_type.rb +21 -0
  17. data/lib/suitcase/hotel/cache.rb +52 -0
  18. data/lib/suitcase/hotel/ean_exception.rb +35 -0
  19. data/lib/suitcase/hotel/helpers.rb +198 -0
  20. data/lib/suitcase/hotel/image.rb +19 -0
  21. data/lib/suitcase/hotel/location.rb +67 -0
  22. data/lib/suitcase/hotel/nightly_rate.rb +14 -0
  23. data/lib/suitcase/hotel/payment_option.rb +41 -0
  24. data/lib/suitcase/hotel/reservation.rb +15 -0
  25. data/lib/suitcase/hotel/room.rb +138 -0
  26. data/lib/suitcase/hotel/session.rb +7 -0
  27. data/lib/suitcase/hotel/surcharge.rb +23 -0
  28. data/lib/suitcase/version.rb +3 -0
  29. data/rthbound-suitcase.gemspec +30 -0
  30. data/test/car_rentals/car_rental_test.rb +30 -0
  31. data/test/hotels/amenity_test.rb +23 -0
  32. data/test/hotels/caching_test.rb +42 -0
  33. data/test/hotels/ean_exception_test.rb +24 -0
  34. data/test/hotels/helpers_test.rb +52 -0
  35. data/test/hotels/hotel_location_test.rb +23 -0
  36. data/test/hotels/hotel_test.rb +112 -0
  37. data/test/hotels/image_test.rb +20 -0
  38. data/test/hotels/payment_option_test.rb +15 -0
  39. data/test/hotels/reservation_test.rb +15 -0
  40. data/test/hotels/room_test.rb +50 -0
  41. data/test/hotels/session_test.rb +14 -0
  42. data/test/keys.rb +43 -0
  43. data/test/minitest_helper.rb +29 -0
  44. data/test/support/fake_response.rb +13 -0
  45. metadata +220 -0
@@ -0,0 +1,14 @@
1
+ module Suitcase
2
+ class NightlyRate
3
+ attr_accessor :promo, :rate, :base_rate
4
+
5
+ # Internal: Create a NightlyRate from the API response.
6
+ #
7
+ # info - A Hash from the API response containing nightly rate information.
8
+ def initialize(info)
9
+ @promo = info["@promo"]
10
+ @rate = info["@rate"]
11
+ @base_rate = info["@baseRate"]
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,41 @@
1
+ module Suitcase
2
+ class Hotel
3
+ class PaymentOption
4
+ attr_accessor :code, :name
5
+
6
+ extend Helpers
7
+
8
+ # Internal: Create a PaymentOption.
9
+ #
10
+ # code - The String code from the API response (e.g. "VI").
11
+ # name - The String name of the PaymentOption.
12
+ def initialize(code, name)
13
+ @code, @name = code, name
14
+ end
15
+
16
+ # Public: Find PaymentOptions for a specific currency.
17
+ #
18
+ # info - A Hash of information with one key: :currency_code.
19
+ #
20
+ # Returns an Array of PaymentOption's.
21
+ def self.find(info)
22
+ params = { "currencyCode" => info[:currency_code] }
23
+
24
+ if Configuration.cache? and Configuration.cache.cached?(:paymentInfo, params)
25
+ types_raw = Configuration.cache.get_query(:paymentInfo, params)
26
+ else
27
+ types_raw = parse_response(url(:method => "paymentInfo", :params => params, :session => info[:session]))
28
+ Configuration.cache.save_query(:paymentInfo, params, types_raw) if Configuration.cache?
29
+ end
30
+ update_session(types_raw, info[:session])
31
+
32
+ types_raw["HotelPaymentResponse"].map do |raw|
33
+ types = raw[0] != "PaymentType" ? [] : raw[1]
34
+ types.map do |type|
35
+ PaymentOption.new(type["code"], type["name"])
36
+ end
37
+ end.flatten
38
+ end
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,15 @@
1
+ module Suitcase
2
+ class Hotel
3
+ class Reservation
4
+ attr_accessor :itinerary_id, :confirmation_numbers, :raw, :surcharges
5
+
6
+ # Internal: Create a new Reservation from the API response.
7
+ #
8
+ # info - The Hash of information returned from the API.
9
+ def initialize(info)
10
+ @itinerary_id, @confirmation_numbers = info[:itinerary_id], [info[:confirmation_numbers]].flatten
11
+ @surcharges = info[:surcharges]
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,138 @@
1
+ module Suitcase
2
+ class Hotel
3
+ class Room
4
+ attr_accessor :rate_key, :hotel_id, :supplier_type, :rate_code,
5
+ :room_type_code, :supplier_type, :tax_rate, :non_refundable,
6
+ :occupancy, :quoted_occupancy, :min_guest_age, :total,
7
+ :surcharge_total, :nightly_rate_total, :average_base_rate,
8
+ :average_rate, :max_nightly_rate, :currency_code, :value_adds,
9
+ :room_type_description, :price_breakdown, :total_price,
10
+ :average_nightly_rate, :promo, :arrival, :departure, :rooms,
11
+ :bed_types, :cancellation_policy, :non_refundable,
12
+ :guarantee_required, :deposit_required, :surcharges,
13
+ :rate_description, :raw, :rate_change, :guarantee_only
14
+
15
+ extend Helpers
16
+ include Helpers
17
+
18
+ # Internal: Create a new Room from within a Room search query.
19
+ #
20
+ # info - A Hash of parsed information from the API, with any of the keys
21
+ # from the attr_accessor's list.
22
+ def initialize(info)
23
+ info.each do |k, v|
24
+ instance_variable_set("@" + k.to_s, v)
25
+ end
26
+ end
27
+
28
+ # Public: Reserve a room.
29
+ #
30
+ # info - A Hash of the information described on the Suitcase
31
+ # [wiki](http://github.com/thoughtfusion/suitcase/wiki/User-flow).
32
+ #
33
+ # Returns a Suitcase::Reservation.
34
+ def reserve!(info)
35
+ params = {}
36
+ params["hotelId"] = @hotel_id
37
+ params["arrivalDate"] = @arrival
38
+ params["departureDate"] = @departure
39
+ params["supplierType"] = @supplier_type
40
+ # Only submit the rateKey if it is a merchant hotel
41
+ params["rateKey"] = @rate_key if @supplier_type == "E"
42
+ params["rateTypeCode"] = @room_type_code
43
+ params["rateCode"] = @rate_code
44
+ params["roomTypeCode"] = @room_type_code
45
+ params["chargeableRate"] = chargeable_rate
46
+ params["email"] = info[:email]
47
+ params["firstName"] = info[:first_name]
48
+ params["lastName"] = info[:last_name]
49
+ params["homePhone"] = info[:home_phone]
50
+ params["workPhone"] = info[:work_phone] if info[:work_phone]
51
+ params["extension"] = info[:work_phone_extension] if info[:work_phone_extension]
52
+ params["faxPhone"] = info[:fax_phone] if info[:fax_phone]
53
+ params["companyName"] = info[:company_name] if info[:company_name]
54
+ if info[:additional_emails]
55
+ params["emailIntineraryList"] = info[:additional_emails].join(",")
56
+ end
57
+ params["creditCardType"] = info[:payment_option].code
58
+ params["creditCardNumber"] = info[:credit_card_number]
59
+ params["creditCardIdentifier"] = info[:credit_card_verification_code]
60
+ expiration_date = Date._parse(info[:credit_card_expiration_date])
61
+ params["creditCardExpirationMonth"] = if expiration_date[:mon] < 10
62
+ "0" + expiration_date[:mon].to_s
63
+ else
64
+ expiration_date[:mon].to_s
65
+ end
66
+ params["creditCardExpirationYear"] = expiration_date[:year].to_s
67
+ params["address1"] = info[:address1]
68
+ params["address2"] = info[:address2] if info[:address2]
69
+ params["address3"] = info[:address3] if info[:address3]
70
+ params["city"] = info[:city]
71
+ @rooms.each_with_index do |room, index|
72
+ index += 1
73
+ params["room#{index}"] = "#{room[:adults].to_s},#{room[:children_ages].join(",")}"
74
+ params["room#{index}FirstName"] = room[:first_name] || params["firstName"] # defaults to the billing
75
+ params["room#{index}LastName"] = room[:last_name] || params["lastName"] # person's name
76
+ params["room#{index}BedTypeId"] = room[:bed_type].id if @supplier_type == "E"
77
+ params["room#{index}SmokingPreference"] = room[:smoking_preference] || "E"
78
+ end
79
+ params["stateProvinceCode"] = info[:province]
80
+ params["countryCode"] = info[:country]
81
+ params["postalCode"] = info[:postal_code]
82
+
83
+ uri = Room.url(
84
+ method: "res",
85
+ params: params,
86
+ include_key: true,
87
+ include_cid: true,
88
+ secure: true
89
+ )
90
+ session = Patron::Session.new
91
+ session.timeout = 30000
92
+ session.base_url = "https://" + uri.host
93
+ res = session.post uri.request_uri, {}
94
+ parsed = JSON.parse res.body
95
+
96
+ reservation_res = parsed["HotelRoomReservationResponse"]
97
+ handle_errors(parsed)
98
+ rate_info = reservation_res["RateInfos"]["RateInfo"]
99
+ surcharges = if @supplier_type == "E" && rate_info["ChargeableRateInfo"]["Surcharges"]
100
+ [rate_info["ChargeableRateInfo"]["Surcharges"]["Surcharge"]].
101
+ flatten.map { |s| Surcharge.parse(s) }
102
+ else
103
+ []
104
+ end
105
+ r = Reservation.new(
106
+ itinerary_id: reservation_res["itineraryId"],
107
+ confirmation_numbers: reservation_res["confirmationNumbers"],
108
+ surcharges: surcharges
109
+ )
110
+ r.raw = parsed
111
+ r
112
+ end
113
+
114
+ # Public: The chargeable rate for the Hotel room.
115
+ #
116
+ # Returns the chargeable rate for the Room.
117
+ def chargeable_rate
118
+ if @supplier_type == "E"
119
+ @total_price
120
+ else
121
+ @max_nightly_rate
122
+ end
123
+ end
124
+
125
+ # Public: The description of the displayed rate.
126
+ #
127
+ # Returns the rate description based on the `rate_change` attribute.
128
+ def room_rate_description
129
+ if @rate_change
130
+ "rate changes during the dates requested and the single nightly rate displayed is the highest nightly rate of the dates requested without taxes and fees."
131
+ else
132
+ "highest single night rate during the dates selected without taxes or fees"
133
+ end
134
+ end
135
+ end
136
+ end
137
+ end
138
+
@@ -0,0 +1,7 @@
1
+ module Suitcase
2
+ class Hotel
3
+ # Public: Hold user session data. A simple Struct provided to be passed in
4
+ # to some of the EAN methods.
5
+ Session = Struct.new(:id, :user_agent, :ip_address, :locale, :currency_code)
6
+ end
7
+ end
@@ -0,0 +1,23 @@
1
+ module Suitcase
2
+ class Hotel
3
+ # Public: A Surcharge represents a single surcharge on a Room.
4
+ class Surcharge
5
+ attr_accessor :amount, :type
6
+ # Internal: Create a new Surcharge.
7
+ #
8
+ # info - A Hash of parsed info from Surcharge.parse.
9
+ def initialize(info)
10
+ @amount, @type = info[:amount], info[:type]
11
+ end
12
+
13
+ # Internal: Parse a Surcharge from the room response.
14
+ #
15
+ # info - A Hash of the parsed JSON relevant to the surhcarge.
16
+ #
17
+ # Returns a Surcharge representing the info.
18
+ def self.parse(info)
19
+ new(amount: info["@amount"], type: info["@type"])
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,3 @@
1
+ module Suitcase
2
+ VERSION = "1.7.1"
3
+ end
@@ -0,0 +1,30 @@
1
+ # -*- encoding: utf-8 -*-
2
+ $:.push File.expand_path("../lib", __FILE__)
3
+ require "suitcase/version"
4
+
5
+ Gem::Specification.new do |s|
6
+ s.name = "rthbound-suitcase"
7
+ s.version = Suitcase::VERSION
8
+ s.authors = ["Walter Nelson", "Tad Hosford"]
9
+ s.email = ["tad@isotope11.com"]
10
+ s.homepage = "http://github.com/rthbound/suitcase"
11
+ s.summary = %q{Locates available hotels and rental cars through Expedia and Hotwire. This is a fork of Walter Nelson's gem - suitcase}
12
+ s.description = %q{Ruby library that utilizes the EAN (Expedia.com) API for locating available hotels and the Hotwire API for rental cars.}
13
+
14
+ s.files = `git ls-files`.split("\n")
15
+ s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
16
+ s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
17
+ s.require_paths = ["lib"]
18
+
19
+ s.add_development_dependency "minitest"
20
+ s.add_development_dependency "mocha"
21
+ s.add_development_dependency "turn"
22
+
23
+ s.add_development_dependency "chronic"
24
+
25
+ s.add_development_dependency "rake"
26
+ s.add_development_dependency "pry"
27
+
28
+ s.add_runtime_dependency "json"
29
+ s.add_runtime_dependency "patron"
30
+ end
@@ -0,0 +1,30 @@
1
+ require "minitest_helper"
2
+
3
+ describe Suitcase::CarRental do
4
+ before :each do
5
+ info = {
6
+ destination: "Seattle",
7
+ start_date: "07/04/2012",
8
+ end_date: "07/11/2012",
9
+ pickup_time: "07:30",
10
+ dropoff_time: "11:30"
11
+ }
12
+ @rentals = Suitcase::CarRental.find(info)
13
+ @rental = @rentals.first
14
+ end
15
+
16
+ [:seating, :type_name, :type_code, :possible_features,
17
+ :possible_models].each do |accessor|
18
+ it "has an accessor #{accessor}" do
19
+ @rental.must_respond_to(accessor)
20
+ @rental.must_respond_to((accessor.to_s + "=").to_sym)
21
+ end
22
+ end
23
+
24
+ describe ".find" do
25
+ it "returns an Array of CarRental's" do
26
+ @rentals.must_be_kind_of(Array)
27
+ @rental.must_be_kind_of(Suitcase::CarRental)
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,23 @@
1
+ require "minitest_helper"
2
+
3
+ describe Suitcase::Hotel::Amenity do
4
+ describe ".parse_mask" do
5
+ describe "when provided bitmask is not nil or 0" do
6
+ it "returns an Array of Symbols representing given Amenities" do
7
+ Suitcase::Hotel::Amenity.parse_mask(5).must_equal [:business_services, :hot_tub]
8
+ end
9
+ end
10
+
11
+ describe "when bitmask is 0" do
12
+ it "returns an empty Array" do
13
+ Suitcase::Hotel::Amenity.parse_mask(0).must_equal []
14
+ end
15
+ end
16
+
17
+ describe "when provided bitmask is nil" do
18
+ it "returns nil" do
19
+ Suitcase::Hotel::Amenity.parse_mask(nil).must_equal nil
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,42 @@
1
+ require "minitest_helper"
2
+
3
+ describe Suitcase do
4
+ before :each do
5
+ Suitcase::Configuration.cache = {}
6
+ end
7
+
8
+ it "caches all non-secure queries" do
9
+ # Query 1
10
+ hotel = Suitcase::Hotel.find(id: 123904)
11
+
12
+ # Query 2
13
+ Suitcase::Hotel.find(location: "Boston, US")
14
+
15
+ # Query 3
16
+ room = hotel.rooms(arrival: Keys::RESERVATION_START_TIME, departure: Keys::RESERVATION_END_TIME).first
17
+
18
+ # Query 4
19
+ Suitcase::Hotel::PaymentOption.find currency_code: "USD"
20
+
21
+ # Query 5, don't cache though
22
+ room.rooms[0][:bed_type] = room.bed_types[0]
23
+ room.rooms[0][:smoking_preference] = "NS"
24
+ room.reserve!(Keys::VALID_RESERVATION_INFO)
25
+
26
+ Suitcase::Configuration.cache.keys.count.must_equal 4
27
+ end
28
+
29
+ it "retrieves from the cache if it's there" do
30
+ hotel = Suitcase::Hotel.find(id: 123904)
31
+ Suitcase::Hotel.find(location: "Boston, US")
32
+ hotel.rooms(arrival: Keys::RESERVATION_START_TIME, departure: Keys::RESERVATION_END_TIME)
33
+ Suitcase::Hotel::PaymentOption.find currency_code: "USD"
34
+
35
+ # Disable API access
36
+ Net::HTTP.expects(:get_response).never
37
+ hotel = Suitcase::Hotel.find(id: 123904)
38
+ Suitcase::Hotel.find(location: "Boston, US")
39
+ hotel.rooms(arrival: Keys::RESERVATION_START_TIME, departure: Keys::RESERVATION_END_TIME)
40
+ Suitcase::Hotel::PaymentOption.find currency_code: "USD"
41
+ end
42
+ end
@@ -0,0 +1,24 @@
1
+ require "minitest_helper"
2
+
3
+ describe Suitcase::Hotel::EANException do
4
+ before :each do
5
+ @exception = Suitcase::Hotel::EANException.new(nil)
6
+ end
7
+
8
+ it "has an accessor recovery" do
9
+ @exception.must_respond_to(:recovery)
10
+ @exception.must_respond_to(:recovery=)
11
+ end
12
+
13
+ describe "#recoverable" do
14
+ it "returns true if the recovery attribute is set" do
15
+ @exception.recovery = { locations: ["London", "New London"] }
16
+ @exception.recoverable?.must_equal(true)
17
+ end
18
+
19
+ it "returns false if the recovery attribute is not set" do
20
+ @exception.recovery = nil
21
+ @exception.recoverable?.must_equal(false)
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,52 @@
1
+ require "minitest_helper"
2
+
3
+ describe Suitcase::Hotel::Helpers do
4
+ before :each do
5
+ class Dummy; extend Suitcase::Hotel::Helpers; end
6
+ end
7
+
8
+ describe "#url" do
9
+ it "returns a URI with the proper base URL" do
10
+ url = Dummy.url(method: "action", params: {})
11
+ url.must_be_kind_of(URI)
12
+ url.host.must_match(/api.ean.com/)
13
+ end
14
+
15
+ describe "when using digital signature authorization" do
16
+ it "adds a 'sig' parameter" do
17
+ Suitcase::Configuration.expects(:use_signature_auth?).returns(true)
18
+ Dummy.expects(:generate_signature).returns("test")
19
+
20
+ url = Dummy.url(method: "action", params: {})
21
+ url.query.must_match(/sig=test/)
22
+ end
23
+ end
24
+ end
25
+
26
+ describe "#parse_response" do
27
+ it "raises an exception when passed an invalid URI" do
28
+ proc do
29
+ Dummy.parse_response(URI.parse("http://google.com"))
30
+ end.must_raise JSON::ParserError
31
+ end
32
+
33
+ it "raises an error if a 403 code is received" do
34
+ proc do
35
+ response = FakeResponse.new(code: 403, body: "<h1>An error occurred.</h1>")
36
+ Net::HTTP.stubs(:get_response).returns(response)
37
+ Dummy.parse_response(URI.parse("http://fake.response.will.be.used"))
38
+ end.must_raise Suitcase::Hotel::EANException
39
+ end
40
+ end
41
+
42
+ describe "#generate_signature" do
43
+ it "returns the encrypted API key, shared secret, and timestamp" do
44
+ Suitcase::Configuration.expects(:hotel_api_key).returns("abc")
45
+ Suitcase::Configuration.expects(:hotel_shared_secret).returns("123")
46
+ Time.expects(:now).returns("10")
47
+
48
+ signature = Dummy.generate_signature
49
+ signature.must_equal(Digest::MD5.hexdigest("abc12310"))
50
+ end
51
+ end
52
+ end