usps-imis-api 0.11.23 → 1.0.0.pre.rc.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 (68) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/main.yml +57 -0
  3. data/.gitignore +4 -0
  4. data/.rspec +2 -0
  5. data/.rubocop.yml +88 -0
  6. data/.ruby-version +1 -0
  7. data/.simplecov +8 -0
  8. data/Gemfile +12 -0
  9. data/Gemfile.lock +95 -0
  10. data/Rakefile +12 -0
  11. data/Readme.md +191 -19
  12. data/bin/console +21 -0
  13. data/bin/setup +8 -0
  14. data/lib/ext/hash.rb +10 -0
  15. data/lib/usps/imis/api.rb +138 -177
  16. data/lib/usps/imis/config.rb +10 -68
  17. data/lib/usps/imis/error/api.rb +26 -0
  18. data/lib/usps/imis/error/mapper.rb +9 -0
  19. data/lib/usps/imis/{errors/response_error.rb → error/response.rb} +7 -34
  20. data/lib/usps/imis/mapper.rb +21 -90
  21. data/lib/usps/imis/panel/base_panel.rb +42 -0
  22. data/lib/usps/imis/panel/education.rb +111 -0
  23. data/lib/usps/imis/panel/vsc.rb +109 -0
  24. data/lib/usps/imis/version.rb +1 -1
  25. data/lib/usps/imis.rb +17 -32
  26. data/spec/lib/usps/imis/api_spec.rb +143 -0
  27. data/spec/lib/usps/imis/config_spec.rb +33 -0
  28. data/spec/lib/usps/imis/error/api_spec.rb +17 -0
  29. data/spec/lib/usps/imis/error/response_spec.rb +107 -0
  30. data/spec/lib/usps/imis/mapper_spec.rb +31 -0
  31. data/spec/lib/usps/imis/panel/base_panel_spec.rb +32 -0
  32. data/spec/lib/usps/imis/panel/education_spec.rb +55 -0
  33. data/spec/lib/usps/imis/panel/vsc_spec.rb +38 -0
  34. data/spec/lib/usps/imis_spec.rb +11 -0
  35. data/spec/spec_helper.rb +35 -0
  36. data/usps-imis-api.gemspec +18 -0
  37. metadata +33 -94
  38. data/bin/imis +0 -8
  39. data/lib/usps/imis/business_object.rb +0 -209
  40. data/lib/usps/imis/command_line/interface.rb +0 -204
  41. data/lib/usps/imis/command_line/options_parser.rb +0 -136
  42. data/lib/usps/imis/command_line.rb +0 -15
  43. data/lib/usps/imis/data.rb +0 -76
  44. data/lib/usps/imis/error.rb +0 -55
  45. data/lib/usps/imis/errors/api_error.rb +0 -11
  46. data/lib/usps/imis/errors/command_line_error.rb +0 -11
  47. data/lib/usps/imis/errors/config_error.rb +0 -11
  48. data/lib/usps/imis/errors/locked_id_error.rb +0 -15
  49. data/lib/usps/imis/errors/mapper_error.rb +0 -29
  50. data/lib/usps/imis/errors/missing_id_error.rb +0 -15
  51. data/lib/usps/imis/errors/not_found_error.rb +0 -11
  52. data/lib/usps/imis/errors/panel_unimplemented_error.rb +0 -34
  53. data/lib/usps/imis/errors/unexpected_property_type_error.rb +0 -31
  54. data/lib/usps/imis/logger.rb +0 -19
  55. data/lib/usps/imis/logger_formatter.rb +0 -32
  56. data/lib/usps/imis/logger_helpers.rb +0 -17
  57. data/lib/usps/imis/mocks/business_object.rb +0 -47
  58. data/lib/usps/imis/mocks.rb +0 -11
  59. data/lib/usps/imis/panels/base_panel.rb +0 -158
  60. data/lib/usps/imis/panels/education.rb +0 -33
  61. data/lib/usps/imis/panels/vsc.rb +0 -32
  62. data/lib/usps/imis/panels.rb +0 -25
  63. data/lib/usps/imis/properties.rb +0 -50
  64. data/lib/usps/imis/query.rb +0 -153
  65. data/lib/usps/imis/requests.rb +0 -68
  66. data/spec/support/usps/vcr/config.rb +0 -47
  67. data/spec/support/usps/vcr/filters.rb +0 -89
  68. data/spec/support/usps/vcr.rb +0 -8
data/lib/usps/imis/api.rb CHANGED
@@ -1,246 +1,207 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative 'requests'
4
- require_relative 'business_object'
5
- require_relative 'mapper'
6
- require_relative 'query'
7
-
8
3
  module Usps
9
4
  module Imis
10
- # The core API wrapper
11
- #
12
5
  class Api
13
- include Requests
14
-
15
- # Endpoint for (re-)authentication requests
16
- #
17
6
  AUTHENTICATION_PATH = 'Token'
7
+ API_PATH = 'api'
8
+ QUERY_PATH = 'api/Query'
9
+ PANELS = Struct.new(:vsc, :education)
18
10
 
19
- # API bearer token
20
- #
21
- attr_reader :token
22
-
23
- # Expiration time for the API bearer token
24
- #
25
- # Used to automatically re-authenticate as needed
26
- #
27
- attr_reader :token_expiration
28
-
29
- # Currently selected iMIS ID for API requests
30
- #
31
- attr_reader :imis_id
32
-
33
- # Whether to lock changes to the selected iMIS ID
34
- #
35
- attr_reader :lock_imis_id
11
+ attr_reader :token, :token_expiration, :imis_id
36
12
 
37
- # Tagged logger
38
- #
39
- attr_reader :logger
40
-
41
- # Create a new instance of +Api+ and provide an existing auth token
42
- #
43
- # @param token [String] Auth token
44
- #
45
- def self.with_token(token)
46
- new.tap do |api|
47
- api.instance_variable_set(:@token, token)
48
- api.instance_variable_set(:@token_expiration, Time.now + 3600) # Greater than the actual lifetime of the token
49
- end
50
- end
51
-
52
- # A new instance of +Api+
53
- #
54
- # @param imis_id [Integer, String] iMIS ID to select immediately on initialization
55
- #
56
- def initialize(imis_id: nil)
13
+ def initialize(skip_authentication: false, imis_id: nil)
14
+ authenticate unless skip_authentication
57
15
  self.imis_id = imis_id if imis_id
58
- @logger ||= Imis.logger('Api')
59
- Imis.config.validate!
60
16
  end
61
17
 
62
18
  # Manually set the current ID, if you already have it for a given member
63
19
  #
64
- # @param id [Integer, String] iMIS ID to select for future requests
65
- #
66
- # @return [Integer] iMIS ID
67
- #
68
20
  def imis_id=(id)
69
- raise Errors::LockedIdError if lock_imis_id
70
-
71
- @imis_id = id&.to_i
21
+ @imis_id = id.to_i.to_s
72
22
  end
73
23
 
74
24
  # Convert a member's certificate number into an iMIS ID number
75
25
  #
76
- # @param certificate [String] Certificate number to lookup the corresponding iMIS ID for
77
- #
78
- # @return [Integer] Corresponding iMIS ID
79
- #
80
26
  def imis_id_for(certificate)
81
- raise Errors::LockedIdError if lock_imis_id
82
-
83
- logger.debug "Fetching iMIS ID for #{certificate}"
84
-
85
- begin
86
- result = query(Imis.configuration.imis_id_query_name, { certificate: })
87
- page = result.page.tap { logger.tagged('Response').debug it }
88
- self.imis_id = page.first['ID'].to_i
89
- rescue StandardError
90
- raise Errors::NotFoundError, 'Member not found'
91
- end
27
+ result = query(Imis.configuration.imis_id_query_name, { certificate: })
28
+ @imis_id = result['Items']['$values'][0]['ID']
29
+ rescue StandardError
30
+ raise Error::Api, 'Member not found'
92
31
  end
93
32
 
94
33
  # Run requests as DSL, with specific iMIS ID only maintained for this scope
95
34
  #
96
- # Either +id+ or +certificate+ is required.
97
- #
98
- # While in this block, changes to the value of +imis_id+ are not allowed
99
- #
100
- # If no block is given, this sets the iMIS ID and returns self.
101
- #
102
- # @param id [Integer, String] iMIS ID to select for requests within the block
103
- # @param certificate [String] Certificate number to convert to iMIS ID and select for requests within the block
35
+ # This should be used with methods that do not change the value of `imis_id`
104
36
  #
105
- # @example
106
- # with(12345) do
107
- # update(mm: 15)
108
- # end
109
- #
110
- # @return [Usps::Imis::Api]
111
- #
112
- def with(id = nil, certificate: nil, &)
113
- raise ArgumentError, 'Must provide id or certificate' unless id || certificate
114
-
37
+ def with(id, &)
115
38
  old_id = imis_id
116
-
117
- id.nil? ? imis_id_for(certificate) : self.imis_id = id
118
- return self unless block_given?
119
-
120
- @lock_imis_id = true
39
+ self.imis_id = id
121
40
  instance_eval(&)
122
41
  ensure
123
- if block_given?
124
- @lock_imis_id = false
125
- self.imis_id = old_id
126
- end
42
+ self.imis_id = old_id
127
43
  end
128
44
 
129
- # Build a Query interface
130
- #
131
- # Works with IQA queries and Business Objects
132
- #
133
- # @param query_name [String] Full path of the query, e.g. +$/_ABC/Fiander/iMIS_ID+
134
- # @query_params [Hash] Conforms to pattern +{ param_name => param_value }+
45
+ # Get a business object for the current member
135
46
  #
136
- # @return [Usps::Imis::Query] Query wrapper
137
- #
138
- def query(query_name, query_params = {}) = Query.new(self, query_name, **query_params)
47
+ def get(business_object_name, url_id: nil)
48
+ uri = uri_for(business_object_name, url_id:)
49
+ request = Net::HTTP::Get.new(uri)
50
+ result = submit(uri, authorize(request))
51
+ JSON.parse(result.body)
52
+ end
139
53
 
140
- # Run requests as DSL, with specific +BusinessObject+ only maintained for this scope
54
+ # Update only specific fields on a business object for the current member
141
55
  #
142
- # If no block is given, this returns the specified +BusinessObject+.
56
+ # fields - hash of shape: { field_name => new_value }
143
57
  #
144
- # @param business_object_name [String] Name of the business object
145
- # @param ordinal [Integer] Ordinal to build override ID param of the URL (e.g. used for Panels)
146
- #
147
- # @return [Usps::Imis::BusinessObject]
148
- #
149
- def on(business_object_name, ordinal: nil, &)
150
- object = BusinessObject.new(self, business_object_name, ordinal:)
151
- return object unless block_given?
152
-
153
- object.instance_eval(&)
58
+ def put_fields(business_object_name, fields, url_id: nil)
59
+ updated = filter_fields(business_object_name, fields)
60
+ put(business_object_name, updated, url_id:)
154
61
  end
155
62
 
156
- # An instance of +Mapper+, using this instance as its parent +Api+
63
+ # Update a business object for the current member
157
64
  #
158
- def mapper
159
- @mapper ||= Mapper.new(self)
65
+ def put(business_object_name, body, url_id: nil)
66
+ uri = uri_for(business_object_name, url_id:)
67
+ request = Net::HTTP::Put.new(uri)
68
+ request.body = JSON.dump(body)
69
+ result = submit(uri, authorize(request))
70
+ JSON.parse(result.body)
160
71
  end
161
72
 
162
- # Convenience alias for reading mapped fields
163
- #
164
- # @return Value of the specified field
73
+ # Create a business object for the current member
165
74
  #
166
- def fetch(field_key) = mapper.fetch(field_key)
167
- alias [] fetch
75
+ def post(business_object_name, body, url_id: nil)
76
+ uri = uri_for(business_object_name, url_id:)
77
+ request = Net::HTTP::Post.new(uri)
78
+ request.body = JSON.dump(body)
79
+ result = submit(uri, authorize(request))
80
+ JSON.parse(result.body)
81
+ end
168
82
 
169
- # Convenience alias for reading multiple mapped fields
83
+ # Remove a business object for the current member
170
84
  #
171
- # @return [Array] Values of the specified fields
85
+ # Returns empty string on success
172
86
  #
173
- def fetch_all(*fields) = mapper.fetch_all(*fields)
87
+ def delete(business_object_name, url_id: nil)
88
+ uri = uri_for(business_object_name, url_id:)
89
+ request = Net::HTTP::Delete.new(uri)
90
+ result = submit(uri, authorize(request))
91
+ result.body
92
+ end
174
93
 
175
- # Convenience alias for updating mapped fields
94
+ # Run an IQA Query
176
95
  #
177
- # @return [Array] Array containing the updated object
96
+ # query_name - the full path of the query in IQA, e.g. `$/_ABC/Fiander/iMIS_ID`
97
+ # query_params - hash of shape: { param_name => param_value }
178
98
  #
179
- def put_field(field_key, value) = update(field_key => value)
180
- alias []= put_field
99
+ def query(query_name, query_params = {})
100
+ query_params[:QueryName] = query_name
101
+ path = "#{QUERY_PATH}?#{query_params.to_query}"
102
+ uri = URI(File.join(imis_hostname, path))
103
+ request = Net::HTTP::Get.new(uri)
104
+ result = submit(uri, authorize(request))
105
+ JSON.parse(result.body)
106
+ end
181
107
 
182
- # Convenience alias for updating mapped fields
183
- #
184
- # @return [Usps::Imis::Data] Updated object
185
- #
186
- def update(data) = mapper.update(data)
108
+ def mapper
109
+ @mapper ||= Mapper.new(self)
110
+ end
187
111
 
188
- # List of available Business Object names
189
- #
190
- # @return [Hash<Symbol, Array<String>>] Grouped available Business Objects
191
- #
192
- def business_objects
193
- abc, other = query('BOEntityDefinition').map(&:entity).partition { it.include?('ABC') }
112
+ def panels
113
+ @panels ||= PANELS.new(
114
+ Panel::Vsc.new(self),
115
+ Panel::Education.new(self)
116
+ )
117
+ end
194
118
 
195
- { abc:, other: }
119
+ def update(data)
120
+ mapper.update(data)
196
121
  end
197
122
 
198
- # Convenience accessor for available Panel objects, each using this instance as its parent
199
- # +Api+
200
- #
201
- def panels
202
- @panels ||= Panels.all(self)
123
+ def instance_variables_to_inspect = %i[@token_expiration @imis_id]
124
+
125
+ private
126
+
127
+ def client(uri)
128
+ Net::HTTP.new(uri.host, uri.port).tap do |http|
129
+ http.use_ssl = true
130
+ http.verify_mode = OpenSSL::SSL::VERIFY_PEER
131
+ end
203
132
  end
204
133
 
205
- # Used by the PHP Wrapper to reduce authentication requests
206
- #
207
- def auth_token
208
- authenticate
134
+ def imis_hostname
135
+ Imis.configuration.hostname
136
+ end
209
137
 
210
- { token: @token, token_expiration: @token_expiration }
138
+ # Authorize a request prior to submitting
139
+ #
140
+ # If the current token is missing/expired, request a new one
141
+ #
142
+ def authorize(request)
143
+ authenticate if token_expiration < Time.now
144
+ request.tap { |r| r.add_field('Authorization', "Bearer #{token}") }
211
145
  end
212
146
 
213
- # Ruby 3.5 instance variable filter
147
+ # Construct a business object API endpoint address
214
148
  #
215
- def instance_variables_to_inspect = %i[@token_expiration @imis_id]
149
+ def uri_for(business_object_name, url_id: nil)
150
+ url_id ||= imis_id
151
+ url_id = CGI.escape(url_id)
152
+ URI(File.join(imis_hostname, "#{API_PATH}/#{business_object_name}/#{url_id}"))
153
+ end
216
154
 
217
- private
155
+ def submit(uri, request)
156
+ client(uri).request(request).tap do |result|
157
+ raise Error::Response.from(result) unless result.is_a?(Net::HTTPSuccess)
158
+ end
159
+ end
218
160
 
219
161
  # Authenticate to the iMIS API, and store the access token and expiration time
220
162
  #
221
163
  def authenticate
222
- json = nil # Scope
223
-
224
- logger.tagged('Auth') do
225
- logger.info 'Authenticating with iMIS'
226
-
227
- request = http_post
228
- request.body = URI.encode_www_form(
229
- grant_type: 'password',
230
- username: Imis.configuration.username,
231
- password: Imis.configuration.password
232
- )
233
- result = submit(uri, request)
234
- json = JSON.parse(result.body)
235
- end
164
+ uri = URI(File.join(imis_hostname, AUTHENTICATION_PATH))
165
+ req = Net::HTTP::Post.new(uri)
166
+ authentication_data = {
167
+ grant_type: 'password',
168
+ username: Imis.configuration.username,
169
+ password: Imis.configuration.password
170
+ }
171
+ req.body = URI.encode_www_form(authentication_data)
172
+ result = submit(uri, req)
173
+ json = JSON.parse(result.body)
236
174
 
237
175
  @token = json['access_token']
238
- @token_expiration = Time.now + json['expires_in'] - 60
176
+ @token_expiration = Time.parse(json['.expires'])
239
177
  end
240
178
 
241
- # URI for the authentication endpoint
179
+ # Manually assemble the matching data structure, with fields in the correct order
242
180
  #
243
- def uri(...) = URI(File.join(Imis.configuration.hostname, AUTHENTICATION_PATH))
181
+ def filter_fields(business_object_name, fields)
182
+ existing = get(business_object_name)
183
+
184
+ JSON.parse(JSON.dump(existing)).tap do |updated|
185
+ # The first property is always the iMIS ID again
186
+ updated['Properties']['$values'] = [existing['Properties']['$values'][0]]
187
+
188
+ # Iterate through all existing fields
189
+ existing['Properties']['$values'].each do |value|
190
+ next unless fields.keys.include?(value['Name'])
191
+
192
+ # Strings are not wrapped in the type definition structure
193
+ new_value = fields[value['Name']]
194
+ if new_value.is_a?(String)
195
+ value['Value'] = new_value
196
+ else
197
+ value['Value']['$value'] = new_value
198
+ end
199
+
200
+ # Add the completed field with the updated value
201
+ updated['Properties']['$values'] << value
202
+ end
203
+ end
204
+ end
244
205
  end
245
206
  end
246
207
  end
@@ -1,87 +1,29 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative 'logger'
4
- require_relative 'logger_formatter'
5
- require_relative 'logger_helpers'
6
-
7
3
  module Usps
8
4
  module Imis
9
- # API Configuration
10
- #
11
5
  class Config
12
6
  IMIS_ROOT_URL_PROD = 'https://portal.americasboatingclub.org'
13
7
  IMIS_ROOT_URL_DEV = 'https://abcdev.imiscloud.com'
14
- REQUIRED_CONFIGS = %w[imis_id_query_name username password].freeze
15
8
 
16
- attr_accessor :imis_id_query_name, :username, :password
17
- attr_reader :environment, :logger, :logger_level, :logger_file
9
+ attr_accessor :environment, :imis_id_query_name, :username, :password
18
10
 
19
11
  def initialize
20
- @environment = default_environment
21
- @imis_id_query_name = ENV.fetch('IMIS_ID_QUERY_NAME', nil)
22
- @username = ENV.fetch('IMIS_USERNAME', nil)
23
- @password = ENV.fetch('IMIS_PASSWORD', nil)
24
- @base_logger = Logger.new($stdout, level: :info)
25
- @logger = ActiveSupport::TaggedLogging.new(@base_logger)
26
-
27
12
  yield self if block_given?
28
-
29
- @logger_level = logger.class::SEV_LABEL[logger.level].downcase.to_sym
30
- end
31
-
32
- def environment=(env)
33
- @environment = ActiveSupport::StringInquirer.new(env.to_s)
34
- end
35
-
36
- def logger=(logger)
37
- @base_logger = logger.tap { it.formatter = LoggerFormatter.new }
38
- @base_logger.singleton_class.include(LoggerHelpers)
39
- @logger = ActiveSupport::TaggedLogging.new(@base_logger)
40
- end
41
-
42
- def logger_file=(path)
43
- @logger_file = path
44
- @base_logger = Logger.new(@logger_file.nil? ? $stdout : @logger_file, level: logger.level)
45
- @logger = ActiveSupport::TaggedLogging.new(@base_logger)
46
- end
47
-
48
- def silence!
49
- self.logger = Logger.new(nil)
50
13
  end
51
14
 
52
- # Environment-specific API endpoint hostname
53
- #
54
- # @return The API hostname for the current environment
55
- #
56
15
  def hostname
57
- return IMIS_ROOT_URL_PROD if environment.production?
58
- return IMIS_ROOT_URL_DEV if environment.development?
59
-
60
- raise Errors::ConfigError, "Unexpected API environment: #{environment}"
61
- end
62
-
63
- # Ruby 3.5 instance variable filter
64
- #
65
- def instance_variables_to_inspect = instance_variables - %i[@password @base_logger @logger]
66
-
67
- # Parameters to filter out of logging
68
- #
69
- def filtered_parameters = %i[password]
70
-
71
- def validate!
72
- missing_config = REQUIRED_CONFIGS.filter_map { it if public_send(it).nil? }
73
- return if missing_config.empty?
74
-
75
- raise Errors::ConfigError, "Missing required configuration: #{missing_config.join(', ')}"
16
+ case environment.to_sym
17
+ when :production
18
+ IMIS_ROOT_URL_PROD
19
+ when :development
20
+ IMIS_ROOT_URL_DEV
21
+ else
22
+ raise Error::Api, "Unexpected API environment: #{environment}"
23
+ end
76
24
  end
77
25
 
78
- private
79
-
80
- def default_environment
81
- return ::Rails.env if defined?(::Rails)
82
-
83
- ActiveSupport::StringInquirer.new(ENV.fetch('IMIS_ENVIRONMENT', 'development'))
84
- end
26
+ def instance_variables_to_inspect = %i[@environment @imis_id_query_name @username]
85
27
  end
86
28
  end
87
29
  end
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Usps
4
+ module Imis
5
+ module Error
6
+ class Api < StandardError
7
+ attr_accessor :metadata
8
+
9
+ def initialize(message, metadata = {})
10
+ super(message)
11
+ @metadata = metadata
12
+ end
13
+
14
+ def bugsnag_meta_data
15
+ metadata == {} ? {} : base_metadata
16
+ end
17
+
18
+ private
19
+
20
+ def base_metadata
21
+ { api: metadata }
22
+ end
23
+ end
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Usps
4
+ module Imis
5
+ module Error
6
+ class Mapper < Api; end
7
+ end
8
+ end
9
+ end
@@ -2,51 +2,24 @@
2
2
 
3
3
  module Usps
4
4
  module Imis
5
- module Errors
6
- # Exception raised due to receiving an error response from the API
7
- #
8
- class ResponseError < Error
9
- # [Net::HTTPResponse] The response received from the API
10
- #
5
+ module Error
6
+ class Response < Api
11
7
  attr_reader :response
12
-
13
- # [Hash] Additional call-specific metadata to pass through to Bugsnag
14
- #
15
8
  attr_accessor :metadata
16
9
 
17
- # Create a new instance of +ResponseError+ from an API response
18
- #
19
- # @param response [Net::HTTPResponse] The response received from the API
20
- #
21
- def self.from(response) = new(response)
10
+ def self.from(response)
11
+ new(nil, response)
12
+ end
22
13
 
23
- # Create a new instance of +ResponseError+
24
- #
25
- # @param response [Net::HTTPResponse] The response received from the API
26
- # @param metadata [Hash] Additional call-specific metadata to pass through to Bugsnag
27
- #
28
- def initialize(response, metadata = {})
14
+ def initialize(_message, response, metadata = {})
29
15
  @response = response
30
16
  super(message, metadata)
31
17
  end
32
18
 
33
- # Additional metadata to include in Bugsnag reports
34
- #
35
- # Can include fields at the top level, which will be shows on the +custom+ tab
36
- #
37
- # Can include fields nested under a top-level key, which will be shown on a tab with the
38
- # top-level key as its name
39
- #
40
- # @return [Hash]
41
- #
42
19
  def bugsnag_meta_data
43
- base_metadata.tap { it[:api].merge!(metadata) }
20
+ base_metadata.tap { |m| m[:api].merge!(metadata) }
44
21
  end
45
22
 
46
- # Auto-formatted exception message, based on the provided API response
47
- #
48
- # @return [String] The exception message
49
- #
50
23
  def message
51
24
  [
52
25
  "#{self.class.name}: [#{status.to_s.upcase}] The iMIS API returned an error.",