rthbound-suitcase 1.7.1

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.
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