hungrytable 0.0.8 → 1.0.0

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 (42) hide show
  1. checksums.yaml +7 -0
  2. data/README.md +299 -22
  3. data/RELEASE_NOTES.md +16 -0
  4. data/lib/hungrytable/api_health_monitor.rb +106 -0
  5. data/lib/hungrytable/circuit_breaker.rb +177 -0
  6. data/lib/hungrytable/config.rb +38 -16
  7. data/lib/hungrytable/enhanced_errors.rb +127 -0
  8. data/lib/hungrytable/errors.rb +36 -0
  9. data/lib/hungrytable/get_request.rb +17 -6
  10. data/lib/hungrytable/post_request.rb +17 -6
  11. data/lib/hungrytable/request.rb +42 -5
  12. data/lib/hungrytable/request_extensions.rb +17 -4
  13. data/lib/hungrytable/request_header.rb +25 -25
  14. data/lib/hungrytable/reservation_cancel.rb +19 -5
  15. data/lib/hungrytable/reservation_make.rb +30 -9
  16. data/lib/hungrytable/reservation_status.rb +56 -0
  17. data/lib/hungrytable/restaurant.rb +43 -31
  18. data/lib/hungrytable/restaurant_search.rb +63 -34
  19. data/lib/hungrytable/restaurant_slotlock.rb +32 -10
  20. data/lib/hungrytable/version.rb +3 -1
  21. data/lib/hungrytable.rb +117 -65
  22. metadata +43 -179
  23. data/.gitignore +0 -6
  24. data/.rvmrc +0 -1
  25. data/Gemfile +0 -5
  26. data/Guardfile +0 -16
  27. data/Rakefile +0 -8
  28. data/hungrytable.gemspec +0 -37
  29. data/test/restaurant_get_details_result.json +0 -6
  30. data/test/restaurant_search_result.json +0 -7
  31. data/test/test_helper.rb +0 -18
  32. data/test/unit/config_test.rb +0 -43
  33. data/test/unit/get_request_test.rb +0 -0
  34. data/test/unit/hungrytable/user_test.rb +0 -28
  35. data/test/unit/post_request_test.rb +0 -0
  36. data/test/unit/request_test.rb +0 -0
  37. data/test/unit/reservation_cancel_test.rb +0 -0
  38. data/test/unit/reservation_make_test.rb +0 -0
  39. data/test/unit/restaurant_search_test.rb +0 -0
  40. data/test/unit/restaurant_slotlock_test.rb +0 -0
  41. data/test/unit/restaurant_test.rb +0 -39
  42. data/test/user_login_result.json +0 -6
@@ -1,26 +1,48 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Hungrytable
4
+ # Configuration module for Hungrytable
2
5
  module Config
3
- extend self
6
+ class << self
7
+ attr_writer :partner_id, :oauth_key, :oauth_secret, :base_url
4
8
 
5
- def partner_id
6
- ENV['OT_PARTNER_ID'] || config_error('OT_PARTNER_ID')
7
- end
9
+ def partner_id
10
+ @partner_id ||= ENV.fetch('OT_PARTNER_ID') do
11
+ raise ConfigurationError, 'OT_PARTNER_ID must be set via ENV or Config.partner_id='
12
+ end
13
+ end
8
14
 
9
- def oauth_key
10
- ENV['OT_OAUTH_KEY'] || config_error('OT_OAUTH_KEY')
11
- end
15
+ def oauth_key
16
+ @oauth_key ||= ENV.fetch('OT_OAUTH_KEY') do
17
+ raise ConfigurationError, 'OT_OAUTH_KEY must be set via ENV or Config.oauth_key='
18
+ end
19
+ end
12
20
 
13
- def oauth_secret
14
- ENV['OT_OAUTH_SECRET'] || config_error('OT_OAUTH_SECRET')
15
- end
21
+ def oauth_secret
22
+ @oauth_secret ||= ENV.fetch('OT_OAUTH_SECRET') do
23
+ raise ConfigurationError, 'OT_OAUTH_SECRET must be set via ENV or Config.oauth_secret='
24
+ end
25
+ end
16
26
 
17
- def base_url
18
- 'https://secure.opentable.com/api/otapi_v2.ashx'
19
- end
27
+ def base_url
28
+ @base_url ||= 'https://secure.opentable.com/api/otapi_v3.ashx'
29
+ end
30
+
31
+ # Reset configuration to defaults (useful for testing)
32
+ def reset!
33
+ @partner_id = nil
34
+ @oauth_key = nil
35
+ @oauth_secret = nil
36
+ @base_url = nil
37
+ end
20
38
 
21
- private
22
- def config_error var
23
- raise HungrytableError, "ENV variable #{var} must be set."
39
+ # Check if all required configuration is present
40
+ def valid?
41
+ partner_id && oauth_key && oauth_secret
42
+ true
43
+ rescue ConfigurationError
44
+ false
45
+ end
24
46
  end
25
47
  end
26
48
  end
@@ -0,0 +1,127 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Hungrytable
4
+ # Enhanced error classes with diagnostic information
5
+ module EnhancedErrors
6
+ # Enhance UnauthorizedError with diagnostic info
7
+ class EnhancedUnauthorizedError < UnauthorizedError
8
+ attr_reader :diagnostics
9
+
10
+ def initialize(message = nil)
11
+ @diagnostics = build_diagnostics
12
+ super(build_message(message))
13
+ end
14
+
15
+ private
16
+
17
+ def build_message(original_message)
18
+ msg = original_message || 'Authentication failed. Check your OAuth credentials.'
19
+ msg += "\n\nDiagnostics:\n"
20
+ msg += " Partner ID: #{mask_credential(Config.partner_id)}\n"
21
+ msg += " OAuth Key: #{mask_credential(Config.oauth_key)}\n"
22
+ msg += " Endpoint: #{Config.base_url}\n"
23
+ msg += "\nPossible causes:\n"
24
+ msg += " 1. Invalid or expired OAuth credentials\n"
25
+ msg += " 2. Partner ID does not match OAuth key/secret\n"
26
+ msg += " 3. Credentials not authorized for this API version\n"
27
+ msg += " 4. API endpoint has been deprecated\n"
28
+ msg += "\nRecommended actions:\n"
29
+ msg += " 1. Verify credentials: OT_PARTNER_ID, OT_OAUTH_KEY, OT_OAUTH_SECRET\n"
30
+ msg += " 2. Contact OpenTable: partnersupport@opentable.com\n"
31
+ msg += " 3. Run diagnostic: bin/verify_api\n"
32
+ msg
33
+ end
34
+
35
+ def build_diagnostics
36
+ {
37
+ partner_id: mask_credential(Config.partner_id),
38
+ oauth_key: mask_credential(Config.oauth_key),
39
+ endpoint: Config.base_url,
40
+ timestamp: Time.now.utc.iso8601,
41
+ circuit_breaker_state: CircuitBreaker.state
42
+ }
43
+ end
44
+
45
+ def mask_credential(credential)
46
+ return 'NOT SET' unless credential
47
+ return '***' if credential.length <= 8
48
+
49
+ "#{credential[0..3]}...#{credential[-4..]}"
50
+ end
51
+ end
52
+
53
+ # Enhance ServerError with diagnostic info
54
+ class EnhancedServerError < ServerError
55
+ attr_reader :diagnostics
56
+
57
+ def initialize(message = nil, status_code = nil)
58
+ @status_code = status_code
59
+ @diagnostics = build_diagnostics
60
+ super(build_message(message))
61
+ end
62
+
63
+ private
64
+
65
+ def build_message(original_message)
66
+ msg = original_message || "OpenTable server error (#{@status_code})"
67
+ msg += "\n\nDiagnostics:\n"
68
+ msg += " Endpoint: #{Config.base_url}\n"
69
+ msg += " Status Code: #{@status_code}\n"
70
+ msg += " Circuit Breaker: #{CircuitBreaker.state}\n"
71
+ msg += "\nPossible causes:\n"
72
+ msg += " 1. OpenTable API is experiencing issues\n"
73
+ msg += " 2. Endpoint has been deprecated or moved\n"
74
+ msg += " 3. Rate limiting or quota exceeded\n"
75
+ msg += "\nRecommended actions:\n"
76
+ msg += " 1. Check OpenTable status page\n"
77
+ msg += " 2. Wait and retry (circuit breaker will help)\n"
78
+ msg += " 3. Contact OpenTable support if issue persists\n"
79
+ msg += " 4. Run diagnostic: bin/verify_api\n"
80
+ msg
81
+ end
82
+
83
+ def build_diagnostics
84
+ {
85
+ endpoint: Config.base_url,
86
+ status_code: @status_code,
87
+ timestamp: Time.now.utc.iso8601,
88
+ circuit_breaker_state: CircuitBreaker.state
89
+ }
90
+ end
91
+ end
92
+
93
+ # Enhance HTTPError with retry suggestions
94
+ class EnhancedHTTPError < HTTPError
95
+ attr_reader :diagnostics, :retryable
96
+
97
+ def initialize(message, retryable: false)
98
+ @retryable = retryable
99
+ @diagnostics = build_diagnostics
100
+ super(build_message(message))
101
+ end
102
+
103
+ private
104
+
105
+ def build_message(original_message)
106
+ msg = original_message
107
+ msg += "\n\nRetryable: #{@retryable ? 'Yes' : 'No'}\n"
108
+ msg += "Circuit Breaker State: #{CircuitBreaker.state}\n"
109
+
110
+ if @retryable
111
+ msg += "\nThis error may be transient. Consider retrying.\n"
112
+ end
113
+
114
+ msg
115
+ end
116
+
117
+ def build_diagnostics
118
+ {
119
+ endpoint: Config.base_url,
120
+ timestamp: Time.now.utc.iso8601,
121
+ circuit_breaker_state: CircuitBreaker.state,
122
+ retryable: @retryable
123
+ }
124
+ end
125
+ end
126
+ end
127
+ end
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Hungrytable
4
+ # Base error class for all Hungrytable errors
5
+ class Error < StandardError; end
6
+
7
+ # Configuration errors
8
+ class ConfigurationError < Error; end
9
+
10
+ # API errors
11
+ class APIError < Error
12
+ attr_reader :error_code, :error_message
13
+
14
+ def initialize(error_code, error_message)
15
+ @error_code = error_code
16
+ @error_message = error_message
17
+ super("OpenTable API Error #{error_code}: #{error_message}")
18
+ end
19
+ end
20
+
21
+ # HTTP errors
22
+ class HTTPError < Error; end
23
+ class NotFoundError < HTTPError; end
24
+ class UnauthorizedError < HTTPError; end
25
+ class ServerError < HTTPError; end
26
+
27
+ # Validation errors
28
+ class ValidationError < Error; end
29
+ class MissingRequiredFieldError < ValidationError; end
30
+
31
+ # OpenTable specific errors mapped from error codes
32
+ class ReservationError < APIError; end
33
+ class SlotlockError < APIError; end
34
+ class RestaurantNotFoundError < APIError; end
35
+ class NoAvailabilityError < APIError; end
36
+ end
@@ -1,15 +1,26 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Hungrytable
4
+ # HTTP GET request handler
2
5
  class GetRequest < Request
6
+ # Default timeout for HTTP requests (in seconds)
7
+ DEFAULT_TIMEOUT = 30
8
+
3
9
  private
4
- def auth_header
5
- Hungrytable::RequestHeader.new(:get, @uri, {}, {})
6
- end
7
10
 
8
11
  def make_request
9
- Curl::Easy.perform(@uri) do |curl|
10
- curl.headers['Authorization'] = auth_header
11
- end
12
+ response = HTTP
13
+ .timeout(DEFAULT_TIMEOUT)
14
+ .headers('Authorization' => auth_header)
15
+ .get(uri)
16
+
17
+ handle_http_errors(response)
18
+ rescue HTTP::Error => e
19
+ raise HTTPError, "HTTP request failed: #{e.message}"
12
20
  end
13
21
 
22
+ def auth_header
23
+ RequestHeader.new(:get, uri, {}, {}).to_s
24
+ end
14
25
  end
15
26
  end
@@ -1,15 +1,26 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Hungrytable
4
+ # HTTP POST request handler
2
5
  class PostRequest < Request
6
+ # Default timeout for HTTP requests (in seconds)
7
+ DEFAULT_TIMEOUT = 30
8
+
3
9
  private
4
- def auth_header
5
- Hungrytable::RequestHeader.new(:post, @uri, {}, {})
6
- end
7
10
 
8
11
  def make_request
9
- Curl::Easy.http_post(@uri, @params.to_query) do |curl|
10
- curl.headers['Authorization'] = auth_header
11
- end
12
+ response = HTTP
13
+ .timeout(DEFAULT_TIMEOUT)
14
+ .headers('Authorization' => auth_header)
15
+ .post(uri, form: params)
16
+
17
+ handle_http_errors(response)
18
+ rescue HTTP::Error => e
19
+ raise HTTPError, "HTTP request failed: #{e.message}"
12
20
  end
13
21
 
22
+ def auth_header
23
+ RequestHeader.new(:post, uri, params, {}).to_s
24
+ end
14
25
  end
15
26
  end
@@ -1,17 +1,54 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Hungrytable
4
+ # Base class for API requests
2
5
  class Request
3
- def initialize uri, params={}
4
- @uri = Hungrytable::Config.base_url << uri
6
+ attr_reader :uri, :params
7
+
8
+ def initialize(uri, params = {})
9
+ @uri = "#{Hungrytable::Config.base_url}#{uri}" # Fixed: don't mutate base_url
5
10
  @params = params
6
11
  end
7
12
 
13
+ # Parse the JSON response
14
+ # @return [Hash] parsed JSON response
15
+ # @raise [Hungrytable::HTTPError] if the response is invalid
8
16
  def parsed_response
9
- JSON.parse(response)
17
+ JSON.parse(response_body)
18
+ rescue JSON::ParserError => e
19
+ raise HTTPError, "Failed to parse response: #{e.message}"
10
20
  end
11
21
 
12
22
  private
13
- def response
14
- @response ||= make_request.body_str
23
+
24
+ # Get the response body from the HTTP request
25
+ # @return [String] response body
26
+ def response_body
27
+ @response_body ||= make_request
28
+ end
29
+
30
+ # Make the HTTP request (to be implemented by subclasses)
31
+ # @return [String] response body
32
+ def make_request
33
+ raise NotImplementedError, 'Subclasses must implement make_request'
34
+ end
35
+
36
+ # Handle HTTP errors
37
+ # @param response [HTTP::Response] the HTTP response object
38
+ # @raise [Hungrytable::HTTPError] if the response indicates an error
39
+ def handle_http_errors(response)
40
+ case response.status.code
41
+ when 200..299
42
+ response.body.to_s
43
+ when 401
44
+ raise UnauthorizedError, 'Authentication failed. Check your OAuth credentials.'
45
+ when 404
46
+ raise NotFoundError, "Resource not found: #{uri}"
47
+ when 500..599
48
+ raise ServerError, "OpenTable server error (#{response.status.code})"
49
+ else
50
+ raise HTTPError, "HTTP error #{response.status.code}: #{response.body}"
51
+ end
15
52
  end
16
53
  end
17
54
  end
@@ -1,19 +1,32 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Hungrytable
4
+ # Shared functionality for classes that make API requests
5
+ # Follows DRY principle - don't repeat yourself
2
6
  module RequestExtensions
3
7
  extend ActiveSupport::Concern
4
8
 
5
9
  private
10
+
11
+ # Get the request object (lazy-loaded)
12
+ # @return [Request] the request object
6
13
  def request
7
14
  @request ||= @requester.new(request_uri, params)
8
15
  end
9
16
 
17
+ # Ensure all required options are present
18
+ # @raise [MissingRequiredFieldError] if any required field is missing
10
19
  def ensure_required_opts
11
- required_opts.each do |key|
12
- raise ArgumentError, "options must include a value for #{key}" unless opts.has_key?(key)
13
- end
20
+ return unless respond_to?(:required_opts, true)
21
+
22
+ missing = required_opts.reject { |key| opts.key?(key) }
23
+ return if missing.empty?
24
+
25
+ raise MissingRequiredFieldError, "Missing required fields: #{missing.join(', ')}"
14
26
  end
15
27
 
16
- # Will be overwritten in objects that send post requests
28
+ # Default params (can be overridden in classes that send POST requests)
29
+ # @return [Hash] request parameters
17
30
  def params
18
31
  {}
19
32
  end
@@ -1,27 +1,28 @@
1
+ # frozen_string_literal: true
2
+
1
3
  # Modified from simple_oauth (https://github.com/laserlemon/simple_oauth)
2
4
  module Hungrytable
3
5
  class RequestHeader
4
-
5
- ATTRIBUTE_KEYS = %w(consumer_key nonce signature_method timestamp token version).map(&:to_sym)
6
+ ATTRIBUTE_KEYS = %w[consumer_key nonce signature_method timestamp token version].map(&:to_sym)
6
7
 
7
8
  def self.default_options
8
9
  {
9
- :nonce => OpenSSL::Random.random_bytes(16).unpack('H*')[0],
10
- :signature_method => 'HMAC-SHA1',
11
- :timestamp => Time.now.to_i.to_s,
12
- :version => '1.0',
13
- :consumer_key => Hungrytable::Config.oauth_key,
14
- :consumer_secret => Hungrytable::Config.oauth_secret,
15
- :token => ''
10
+ nonce: OpenSSL::Random.random_bytes(16).unpack1('H*'),
11
+ signature_method: 'HMAC-SHA1',
12
+ timestamp: Time.now.to_i.to_s,
13
+ version: '1.0',
14
+ consumer_key: Hungrytable::Config.oauth_key,
15
+ consumer_secret: Hungrytable::Config.oauth_secret,
16
+ token: ''
16
17
  }
17
18
  end
18
19
 
19
20
  def self.encode(value)
20
- URI.encode(value.to_s, /[^a-z0-9\-\.\_\~]/i)
21
+ CGI.escape(value.to_s).gsub('+', '%20').gsub('%7E', '~')
21
22
  end
22
23
 
23
24
  def self.decode(value)
24
- URI.decode(value.to_s)
25
+ CGI.unescape(value.to_s)
25
26
  end
26
27
 
27
28
  attr_reader :method, :params, :options
@@ -33,7 +34,7 @@ module Hungrytable
33
34
  @uri.normalize!
34
35
  @uri.fragment = nil
35
36
  @params = params
36
- @options = self.class.default_options.merge(oauth)
37
+ @options = self.class.default_options.merge(oauth)
37
38
  end
38
39
 
39
40
  def url
@@ -43,7 +44,7 @@ module Hungrytable
43
44
  end
44
45
 
45
46
  def to_s
46
- %Q(OAuth realm="http://www.opentable.com/", #{normalized_attributes})
47
+ %(OAuth realm="http://www.opentable.com/", #{normalized_attributes})
47
48
  end
48
49
 
49
50
  def valid?(secrets = {})
@@ -55,38 +56,38 @@ module Hungrytable
55
56
  end
56
57
 
57
58
  def signed_attributes
58
- attributes.merge(:oauth_signature => signature)
59
+ attributes.merge(oauth_signature: signature)
59
60
  end
60
61
 
61
62
  private
62
63
 
63
64
  def normalized_attributes
64
- signed_attributes.sort_by{|k,v| k.to_s }.map{|k,v| %(#{k}="#{self.class.encode(v)}") }.join(', ')
65
+ signed_attributes.sort_by { |k, _v| k.to_s }.map { |k, v| %(#{k}="#{self.class.encode(v)}") }.join(', ')
65
66
  end
66
67
 
67
68
  def attributes
68
- ATTRIBUTE_KEYS.inject({}){|a,k| options.key?(k) ? a.merge(:"oauth_#{k}" => options[k]) : a }
69
+ ATTRIBUTE_KEYS.inject({}) { |a, k| options.key?(k) ? a.merge("oauth_#{k}": options[k]) : a }
69
70
  end
70
71
 
71
72
  def signature
72
- send(options[:signature_method].downcase.tr('-', '_') + '_signature')
73
+ send("#{options[:signature_method].downcase.tr('-', '_')}_signature")
73
74
  end
74
75
 
75
76
  def hmac_sha1_signature
76
- Base64.encode64(OpenSSL::HMAC.digest(OpenSSL::Digest::SHA1.new, secret, signature_base)).chomp.gsub(/\n/, '')
77
+ Base64.encode64(OpenSSL::HMAC.digest(OpenSSL::Digest.new('SHA1'), secret, signature_base)).chomp.gsub("\n", '')
77
78
  end
78
79
 
79
80
  def secret
80
- options.values_at(:consumer_secret, :token_secret).map{|v| self.class.encode(v) }.join('&')
81
+ options.values_at(:consumer_secret, :token_secret).map { |v| self.class.encode(v) }.join('&')
81
82
  end
82
- alias_method :plaintext_signature, :secret
83
+ alias plaintext_signature secret
83
84
 
84
85
  def signature_base
85
- [method, url, normalized_params].map{|v| self.class.encode(v) }.join('&')
86
+ [method, url, normalized_params].map { |v| self.class.encode(v) }.join('&')
86
87
  end
87
88
 
88
89
  def normalized_params
89
- signature_params.map{|p| p.map{|v| self.class.encode(v) } }.sort.map{|p| p.join('=') }.join('&')
90
+ signature_params.map { |p| p.map { |v| self.class.encode(v) } }.sort.map { |p| p.join('=') }.join('&')
90
91
  end
91
92
 
92
93
  def signature_params
@@ -94,16 +95,15 @@ module Hungrytable
94
95
  end
95
96
 
96
97
  def url_params
97
- CGI.parse(@uri.query || '').inject([]){|p,(k,vs)| p + vs.sort.map{|v| [k, v] } }
98
+ CGI.parse(@uri.query || '').inject([]) { |p, (k, vs)| p + vs.sort.map { |v| [k, v] } }
98
99
  end
99
100
 
100
101
  def rsa_sha1_signature
101
- Base64.encode64(private_key.sign(OpenSSL::Digest::SHA1.new, signature_base)).chomp.gsub(/\n/, '')
102
+ Base64.encode64(private_key.sign(OpenSSL::Digest.new('SHA1'), signature_base)).chomp.gsub("\n", '')
102
103
  end
103
104
 
104
105
  def private_key
105
106
  OpenSSL::PKey::RSA.new(options[:consumer_secret])
106
107
  end
107
-
108
108
  end
109
109
  end
@@ -1,30 +1,44 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Hungrytable
4
+ # Cancels an existing restaurant reservation
2
5
  class ReservationCancel
3
6
  include RequestExtensions
4
7
 
5
8
  attr_reader :opts
6
9
 
7
- def initialize opts={}
10
+ def initialize(opts = {})
8
11
  @opts = opts
9
12
  ensure_required_opts
10
13
  @requester = opts[:requester] || GetRequest
11
14
  end
12
15
 
16
+ # Check if the cancellation was successful
17
+ # @return [Boolean] true if no errors
13
18
  def successful?
14
- details["ns:ErrorID"] == "0"
19
+ details['ns:ErrorID'].to_s == '0'
20
+ end
21
+
22
+ # Get error messages if cancellation failed
23
+ # @return [String, nil] error message or nil
24
+ def error_message
25
+ details['ns:ErrorMessage']
15
26
  end
16
27
 
17
28
  private
29
+
18
30
  def request_uri
19
- "/reservation/?pid=#{Config.partner_id}&rid=#{opts[:restaurant_id]}&conf=#{opts[:confirmation_number]}&email=#{CGI.escape(opts[:email_address])}"
31
+ "/reservation/?pid=#{Config.partner_id}&rid=#{opts[:restaurant_id]}&" \
32
+ "conf=#{CGI.escape(opts[:confirmation_number].to_s)}&email=#{CGI.escape(opts[:email_address])}"
20
33
  end
21
34
 
35
+ # @return [Hash] cancellation results from API response
22
36
  def details
23
- request.parsed_response["Results"]
37
+ @details ||= request.parsed_response['Results'] || {}
24
38
  end
25
39
 
26
40
  def required_opts
27
- %w(email_address confirmation_number restaurant_id).map(&:to_sym)
41
+ %i[email_address confirmation_number restaurant_id]
28
42
  end
29
43
  end
30
44
  end
@@ -1,28 +1,43 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Hungrytable
4
+ # Makes a new restaurant reservation
2
5
  class ReservationMake
3
6
  include RequestExtensions
4
7
 
5
8
  attr_reader :restaurant_slotlock, :opts
6
9
 
7
- def initialize restaurant_slotlock, opts={}
10
+ def initialize(restaurant_slotlock, opts = {})
8
11
  @opts = opts
9
12
  ensure_required_opts
10
- @requester = opts[:requester] || PostRequest
13
+ @requester = opts[:requester] || PostRequest
11
14
  @restaurant_slotlock = restaurant_slotlock
12
15
  end
13
16
 
17
+ # Check if the reservation was successful
18
+ # @return [Boolean] true if no errors
14
19
  def successful?
15
- details["ns:ErrorID"] == "0"
20
+ details['ns:ErrorID'].to_s == '0'
16
21
  end
17
22
 
23
+ # Get the confirmation number for the reservation
24
+ # @return [String, nil] confirmation number or nil if unsuccessful
18
25
  def confirmation_number
19
26
  return nil unless successful?
20
- details["ns:ConfirmationNumber"]
27
+
28
+ details['ns:ConfirmationNumber']
29
+ end
30
+
31
+ # Get error messages if reservation failed
32
+ # @return [String, nil] error message or nil
33
+ def error_message
34
+ details['ns:ErrorMessage']
21
35
  end
22
36
 
23
37
  private
38
+
24
39
  def required_opts
25
- %w(email_address firstname lastname phone).map(&:to_sym)
40
+ %i[email_address firstname lastname phone]
26
41
  end
27
42
 
28
43
  def default_options
@@ -30,22 +45,28 @@ module Hungrytable
30
45
  'OTannouncementOption' => '0',
31
46
  'RestaurantEmailOption' => '0',
32
47
  'firsttimediner' => '0',
33
- 'specialinstructions' => 'Have a great time',
48
+ 'specialinstructions' => opts[:specialinstructions] || '',
34
49
  'slotlockid' => restaurant_slotlock.slotlock_id
35
50
  }.merge(restaurant_slotlock.params)
36
51
  end
37
52
 
38
53
  def params
39
- default_options.merge(opts)
54
+ # Filter out internal options (like :requester) before converting to API parameters
55
+ internal_opts = %i[requester]
56
+ api_opts = opts.except(*internal_opts)
57
+
58
+ # Convert symbol keys to strings for API
59
+ user_opts = api_opts.transform_keys(&:to_s)
60
+ default_options.merge(user_opts)
40
61
  end
41
62
 
42
63
  def request_uri
43
64
  "/reservation/?pid=#{Config.partner_id}&st=0"
44
65
  end
45
66
 
67
+ # @return [Hash] reservation results from API response
46
68
  def details
47
- request.parsed_response["MakeResults"]
69
+ @details ||= request.parsed_response['MakeResults'] || {}
48
70
  end
49
-
50
71
  end
51
72
  end