freight_kit 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (47) hide show
  1. checksums.yaml +7 -0
  2. data/Gemfile +5 -0
  3. data/Gemfile.lock +201 -0
  4. data/MIT-LICENSE +31 -0
  5. data/README.md +153 -0
  6. data/VERSION +1 -0
  7. data/accessorial_symbols.txt +95 -0
  8. data/freight_kit.gemspec +58 -0
  9. data/lib/freight_kit/carrier.rb +473 -0
  10. data/lib/freight_kit/carriers.rb +24 -0
  11. data/lib/freight_kit/contact.rb +17 -0
  12. data/lib/freight_kit/error.rb +5 -0
  13. data/lib/freight_kit/errors/document_not_found_error.rb +5 -0
  14. data/lib/freight_kit/errors/expired_credentials_error.rb +5 -0
  15. data/lib/freight_kit/errors/http_error.rb +25 -0
  16. data/lib/freight_kit/errors/invalid_credentials_error.rb +5 -0
  17. data/lib/freight_kit/errors/response_error.rb +16 -0
  18. data/lib/freight_kit/errors/shipment_not_found_error.rb +5 -0
  19. data/lib/freight_kit/errors/unserviceable_accessorials_error.rb +17 -0
  20. data/lib/freight_kit/errors/unserviceable_error.rb +5 -0
  21. data/lib/freight_kit/errors.rb +10 -0
  22. data/lib/freight_kit/model.rb +17 -0
  23. data/lib/freight_kit/models/credential.rb +117 -0
  24. data/lib/freight_kit/models/date_time.rb +37 -0
  25. data/lib/freight_kit/models/document_response.rb +17 -0
  26. data/lib/freight_kit/models/label.rb +13 -0
  27. data/lib/freight_kit/models/location.rb +108 -0
  28. data/lib/freight_kit/models/pickup_response.rb +19 -0
  29. data/lib/freight_kit/models/price.rb +38 -0
  30. data/lib/freight_kit/models/rate.rb +81 -0
  31. data/lib/freight_kit/models/rate_response.rb +15 -0
  32. data/lib/freight_kit/models/response.rb +21 -0
  33. data/lib/freight_kit/models/shipment.rb +66 -0
  34. data/lib/freight_kit/models/shipment_event.rb +38 -0
  35. data/lib/freight_kit/models/tracking_response.rb +75 -0
  36. data/lib/freight_kit/models.rb +17 -0
  37. data/lib/freight_kit/package.rb +313 -0
  38. data/lib/freight_kit/package_item.rb +65 -0
  39. data/lib/freight_kit/packaging.rb +52 -0
  40. data/lib/freight_kit/platform.rb +36 -0
  41. data/lib/freight_kit/shipment_packer.rb +116 -0
  42. data/lib/freight_kit/tariff.rb +29 -0
  43. data/lib/freight_kit/version.rb +5 -0
  44. data/lib/freight_kit.rb +34 -0
  45. data/service_type_symbols.txt +4 -0
  46. data/shipment_event_symbols.txt +17 -0
  47. metadata +453 -0
@@ -0,0 +1,473 @@
1
+ # frozen_string_literal: true
2
+
3
+ module FreightKit
4
+ # Carrier is the abstract base class for all supported carriers.
5
+ #
6
+ # To implement support for a carrier, you should subclass this class and
7
+ # implement all the methods that the carrier supports.
8
+ #
9
+ # @see #create_pickup
10
+ # @see #cancel_shipment
11
+ # @see #find_tracking_info
12
+ # @see #find_rates
13
+ #
14
+ # @!attribute last_request
15
+ # The last request performed against the carrier's API.
16
+ # @see #save_request
17
+ class Carrier
18
+ class << self
19
+ # Whether looking up available services is implemented.
20
+ # @return [Boolean]
21
+ def available_services_implemented?
22
+ false
23
+ end
24
+
25
+ # Whether bill of lading (BOL) requires tracking number at time of pickup.
26
+ # @return [Boolean]
27
+ def bol_requires_tracking_number?
28
+ false
29
+ end
30
+
31
+ # Whether canceling a shipment is implemented.
32
+ # @return [Boolean]
33
+ def cancel_shipment_implemented?
34
+ false
35
+ end
36
+
37
+ # Whether creating a pickup is implemented.
38
+ # @return [Boolean]
39
+ def create_pickup_implemented?
40
+ false
41
+ end
42
+
43
+ # The default location to use for {#valid_credentials?}.
44
+ # @return [FreightKit::Location]
45
+ def default_location
46
+ Location.new(
47
+ address1: '455 N. Rexford Dr.',
48
+ address2: '3rd Floor',
49
+ city: 'Beverly Hills',
50
+ country: 'US',
51
+ fax: '1-310-275-8159',
52
+ phone: '1-310-285-1013',
53
+ state: 'CA',
54
+ zip: '90210',
55
+ )
56
+ end
57
+
58
+ # Whether retrieving an existing rate is implemented.
59
+ # @return [Boolean]
60
+ def find_estimate_implemented?
61
+ false
62
+ end
63
+
64
+ # Whether finding rates is implemented.
65
+ # @return [Boolean]
66
+ def find_rates_implemented?
67
+ false
68
+ end
69
+
70
+ # Whether finding rates with declared value (thus insurance) is implemented.
71
+ # @return [Boolean]
72
+ def find_rates_with_declared_value?
73
+ false
74
+ end
75
+
76
+ # Whether retrieving tracking information is implemented.
77
+ # @return [Boolean]
78
+ def find_tracking_info_implemented?
79
+ false
80
+ end
81
+
82
+ # Whether retrieving tracking number from pickup number is implemented.
83
+ # @return [Boolean]
84
+ def find_tracking_number_from_pickup_number_implemented?
85
+ false
86
+ end
87
+
88
+ # The address field maximum length accepted by the carrier
89
+ # @return [Integer]
90
+ def maximum_address_field_length
91
+ 255
92
+ end
93
+
94
+ # The maximum height the carrier will accept.
95
+ # @return [Measured::Length]
96
+ def maximum_height
97
+ Measured::Length.new(105, :inches)
98
+ end
99
+
100
+ # The maximum weight the carrier will accept.
101
+ # @return [Measured::Weight]
102
+ def maximum_weight
103
+ Measured::Weight.new(10_000, :pounds)
104
+ end
105
+
106
+ # What length overlength fees the carrier begins charging at.
107
+ # @return [Measured::Length]
108
+ def minimum_length_for_overlength_fees
109
+ Measured::Length.new(48, :inches)
110
+ end
111
+
112
+ # Whether or not the carrier quotes overlength fees via API.
113
+ # @note Should the API not calculate these fees, they should be calculated some other way outside of FreightKit.
114
+ # @return [Boolean]
115
+ def overlength_fees_require_tariff?
116
+ true
117
+ end
118
+
119
+ # Whether carrier considers pickup number the same as the tracking number.
120
+ def pickup_number_is_tracking_number?
121
+ false
122
+ end
123
+
124
+ # Whether proof of delivery (POD) retrieval is implemented.
125
+ # @return [Boolean]
126
+ def pod_implemented?
127
+ false
128
+ end
129
+
130
+ # Returns the keywords passed to `#initialize` that cannot be blank.
131
+ # @return [Array<Symbol>]
132
+ def requirements
133
+ []
134
+ end
135
+
136
+ # Returns the `Credential` methods (passed to `#initialize`) that cannot respond with blank values.
137
+ # @return [Array<Symbol>]
138
+ def required_credential_types
139
+ %i[api]
140
+ end
141
+
142
+ # Whether scanned bill of lading (BOL) retrieval is implemented.
143
+ # @return [Boolean]
144
+ def scanned_bol_implemented?
145
+ false
146
+ end
147
+ end
148
+
149
+ attr_accessor :conf, :rates_with_excessive_length_fees, :tmpdir
150
+ attr_reader :credentials, :customer_location, :last_request, :tariff
151
+
152
+ # @param credentials [Array<Credential>]
153
+ # @param customer_location [Location]
154
+ # @param tariff [Tariff]
155
+ def initialize(credentials, customer_location: nil, tariff: nil)
156
+ credentials = [credentials] if credentials.is_a?(Credential)
157
+
158
+ if credentials.map(&:class).uniq != [Credential]
159
+ message = "#{self.class.name}#new: `credentials` must be a Credential or Array of Credential"
160
+ raise ArgumentError, message
161
+ end
162
+
163
+ missing_credential_types = self.class.required_credential_types.uniq - credentials.map(&:type).uniq
164
+
165
+ if missing_credential_types.any?
166
+ message = "#{self.class.name}#new: `Credential` of type(s) missing: #{missing_credential_types.join(", ")}"
167
+ raise ArgumentError, message
168
+ end
169
+
170
+ @credentials = credentials
171
+
172
+ if customer_location.present?
173
+ unless customer_location.is_a?(Location)
174
+ message = "#{self.class.name}#new: `customer_location` must be a Location"
175
+ raise ArgumentError, message
176
+ end
177
+
178
+ @customer_location = customer_location
179
+ end
180
+
181
+ if tariff.present?
182
+ raise ArgumentError, "#{self.class.name}#new: `tariff` must be a Tariff" unless tariff.is_a?(Tariff)
183
+
184
+ @tariff = tariff
185
+ end
186
+
187
+ conf_path = File
188
+ .join(
189
+ File.expand_path(
190
+ '../../../../configuration/carriers',
191
+ self.class.const_source_location(:REACTIVE_FREIGHT_CARRIER).first,
192
+ ),
193
+ "#{self.class.to_s.split("::")[1].underscore}.yml",
194
+ )
195
+ @conf = YAML.safe_load(File.read(conf_path), permitted_classes: [Symbol])
196
+
197
+ @rates_with_excessive_length_fees = @conf.dig(:attributes, :rates, :with_excessive_length_fees)
198
+ end
199
+
200
+ # Asks the carrier for the scanned proof of delivery that the carrier would provide after delivery.
201
+ #
202
+ # @param [String] tracking_number Tracking number.
203
+ # @return [DocumentResponse]
204
+ def pod(tracking_number)
205
+ raise NotImplementedError, "#{self.class.name}: #pod not supported"
206
+ end
207
+
208
+ # Asks the carrier for the bill of lading that the carrier would provide before shipping.
209
+ #
210
+ # @see #scanned_bol
211
+ #
212
+ # @param [String] tracking_number Tracking number.
213
+ # @return [DocumentResponse]
214
+ def bol(tracking_number)
215
+ raise NotImplementedError, "#{self.class.name}: #bol not supported"
216
+ end
217
+
218
+ # Asks the carrier for the scanned bill of lading that the carrier would provide after shipping.
219
+ #
220
+ # @see #bol
221
+ #
222
+ # @param [String] tracking_number Tracking number.
223
+ # @return [DocumentResponse]
224
+ def scanned_bol(tracking_number)
225
+ raise NotImplementedError, "#{self.class.name}: #scanned_bol not supported"
226
+ end
227
+
228
+ def find_estimate(*)
229
+ raise NotImplementedError, "#{self.class.name}: #find_estimate not supported"
230
+ end
231
+
232
+ # Asks the carrier for a list of locations (terminals) for a given country
233
+ #
234
+ # @param [ActiveUtils::Country] country
235
+ # @return [Array<Location>]
236
+ def find_locations(country)
237
+ raise NotImplementedError, "#{self.class.name}: #find_locations not supported"
238
+ end
239
+
240
+ def find_tracking_number_from_pickup_number(pickup_number, date)
241
+ raise NotImplementedError, "#{self.class.name}: #find_tracking_number_from_pickup_number not supported"
242
+ end
243
+
244
+ # Asks the carrier for rate estimates for a given shipment.
245
+ #
246
+ # @note Override with whatever you need to get the rates from the carrier.
247
+ #
248
+ # @param shipment [FreightKit::Shipment] Shipment details.
249
+ # @return [FreightKit::RateResponse] The response from the carrier, which
250
+ # includes 0 or more rate estimates for different shipping products
251
+ def find_rates(shipment:)
252
+ raise NotImplementedError, "#find_rates is not supported by #{self.class.name}."
253
+ end
254
+
255
+ # Registers a new pickup with the carrier, to get a tracking number and
256
+ # potentially shipping labels
257
+ #
258
+ # @note Override with whatever you need to register a shipment, and obtain
259
+ # shipping labels if supported by the carrier.
260
+ #
261
+ # @param delivery_from [ActiveSupport::TimeWithZone] Local date, time and time zone that
262
+ # delivery hours begin.
263
+ # @param delivery_to [ActiveSupport::TimeWithZone] Local date, time and time zone that
264
+ # delivery hours end.
265
+ # @param dispatcher [FreightKit::Contact] The dispatcher.
266
+ # @param pickup_from [ActiveSupport::TimeWithZone] Local date, time and time zone that
267
+ # pickup hours begin.
268
+ # @param pickup_to [ActiveSupport::TimeWithZone] Local date, time and time zone that
269
+ # pickup hours end.
270
+ # @param scac [String] The carrier SCAC code (can be `nil`; only used for brokers).
271
+ # @param service [Symbol] The service type.
272
+ # @param shipment [FreightKit::Shipment] The shipment including `#destination.contact`, `#origin.contact`.
273
+ # @return [FreightKit::ShipmentResponse] The response from the carrier. This
274
+ # response should include a shipment identifier or tracking_number if successful,
275
+ # and potentially shipping labels.
276
+ def create_pickup(
277
+ delivery_from:,
278
+ delivery_to:,
279
+ dispatcher:,
280
+ pickup_from:,
281
+ pickup_to:,
282
+ scac:,
283
+ service:,
284
+ shipment:
285
+ )
286
+ raise NotImplementedError, "#create_pickup is not supported by #{self.class.name}."
287
+ end
288
+
289
+ # Cancels a shipment with a carrier.
290
+ #
291
+ # @note Override with whatever you need to cancel a shipping label
292
+ #
293
+ # @param tracking_number [String] The tracking number of the shipment to cancel.
294
+ # @return [FreightKit::Response] The response from the carrier. This
295
+ # response in most cases has a cancellation id.
296
+ def cancel_shipment(tracking_number)
297
+ raise NotImplementedError, "#cancel_shipment is not supported by #{self.class.name}."
298
+ end
299
+
300
+ # Retrieves tracking information for a previous shipment
301
+ #
302
+ # @note Override with whatever you need to get a shipping label
303
+ #
304
+ # @param tracking_number [String] The tracking number of the shipment to track.
305
+ # @return [FreightKit::TrackingResponse] The response from the carrier. This
306
+ # response should a list of shipment tracking events if successful.
307
+ def find_tracking_info(tracking_number)
308
+ raise NotImplementedError, "#find_tracking_info is not supported by #{self.class.name}."
309
+ end
310
+
311
+ # Get a list of services available for the a specific route.
312
+ #
313
+ # @param origin [Location] The origin location.
314
+ # @param destination [Location] The destination location.
315
+ # @return [Array<Symbol>] A list of service type symbols for the available services.
316
+ #
317
+ def available_services(origin, destination)
318
+ raise NotImplementedError, "#available_services is not supported by #{self.class.name}."
319
+ end
320
+
321
+ # Fetch credential of given type.
322
+ #
323
+ # @param type [Symbol] Type of credential to find, should be one of: `:api`, `:selenoid`, `:website`.
324
+ # @return [FreightKit::Credential|NilClass]
325
+ def fetch_credential(type)
326
+ @fetch_credentials ||= {}
327
+ return @fetch_credentials[type] if @fetch_credentials[type].present?
328
+
329
+ @fetch_credentials[type] ||= credentials.find { |credential| credential.type == type }
330
+ end
331
+
332
+ # Validate credentials with a call to the API.
333
+ #
334
+ # By default this just does a `find_rates` call with the origin and destination both as
335
+ # the carrier's default_location. Override to provide alternate functionality.
336
+ #
337
+ # @return [Boolean] Should return `true` if the provided credentials proved to work,
338
+ # `false` otherswise.
339
+ def valid_credentials?
340
+ location = self.class.default_location
341
+ find_rates(location, location, Package.new(100, [5, 15, 30]))
342
+ rescue FreightKit::ResponseError
343
+ false
344
+ else
345
+ true
346
+ end
347
+
348
+ # Validate the tracking number (may call API).
349
+ #
350
+ # @param [String] tracking_number Tracking number.
351
+ # @return [Boolean] Should return `true` if the provided pro is valid.
352
+ def valid_tracking_number?(tracking_number)
353
+ raise NotImplementedError, "#valid_pro is not supported by #{self.class.name}."
354
+ end
355
+
356
+ def overlength_fee(tarrif, package)
357
+ max_dimension_inches = [package.length(:inches), package.width(:inches)].max
358
+
359
+ return 0 if max_dimension_inches < self.class.minimum_length_for_overlength_fees.convert_to(:inches).value
360
+
361
+ tarrif.overlength_rules.each do |overlength_rule|
362
+ next if max_dimension_inches < overlength_rule[:min_length].convert_to(:inches).value
363
+
364
+ if overlength_rule[:max_length].blank? ||
365
+ max_dimension_inches <= overlength_rule[:max_length].convert_to(:inches).value
366
+ return (package.quantity * overlength_rule[:fee_cents])
367
+ end
368
+ end
369
+
370
+ 0
371
+ end
372
+
373
+ # Determine whether the carrier will accept the packages based on credentials and/or tariff.
374
+ # @param packages [Array<FreightKit::Package>]
375
+ # @param tariff [FreightKit::Tariff]
376
+ # @return [Boolean]
377
+ def validate_packages(packages, tariff = nil)
378
+ return false if packages.blank?
379
+
380
+ message = []
381
+
382
+ max_height_inches = self.class.maximum_height.convert_to(:inches).value
383
+ if packages.map { |p| p.height(:inches) }.max > max_height_inches
384
+ message << "items must be #{max_height_inches.to_f} inches tall or less"
385
+ end
386
+
387
+ max_weight_pounds = self.class.maximum_weight.convert_to(:pounds).value
388
+ if packages.sum { |p| p.pounds(:total) } > max_weight_pounds
389
+ message << "items must weigh #{max_weight_pounds.to_f} lbs or less"
390
+ end
391
+
392
+ if self.class.overlength_fees_require_tariff? && (tariff.blank? || !tariff.is_a?(FreightKit::Tariff))
393
+ missing_dimensions = packages.map do |p|
394
+ [p.height(:inches), p.length(:inches), p.width(:inches)].any?(&:zero?)
395
+ end.any?(true)
396
+
397
+ if missing_dimensions
398
+ message << 'item dimensions are required'
399
+ else
400
+ max_length_inches = self.class.minimum_length_for_overlength_fees.convert_to(:inches).value
401
+
402
+ if packages.map { |p| [p.width(:inches), p.length(:inches)].max }.max >= max_length_inches
403
+ message << 'tariff must be defined to calculate overlength fees'
404
+ end
405
+ end
406
+ end
407
+
408
+ raise UnserviceableError, message.join(', ').capitalize if message.present?
409
+
410
+ true
411
+ end
412
+
413
+ def serviceable_accessorials?(accessorials)
414
+ return true if accessorials.blank?
415
+
416
+ unless self.class::REACTIVE_FREIGHT_CARRIER
417
+ raise NotImplementedError, "#{self.class.name}: #serviceable_accessorials? not supported"
418
+ end
419
+
420
+ return false if @conf.dig(:accessorials, :mappable).blank?
421
+
422
+ conf_mappable_accessorials = @conf.dig(:accessorials, :mappable)
423
+ conf_unquotable_accessorials = @conf.dig(:accessorials, :unquotable)
424
+ conf_unserviceable_accessorials = @conf.dig(:accessorials, :unserviceable)
425
+
426
+ unserviceable_accessorials = []
427
+
428
+ accessorials.each do |accessorial|
429
+ if conf_unserviceable_accessorials.present? && conf_unserviceable_accessorials.any?(accessorial)
430
+ unserviceable_accessorials << accessorial
431
+ next
432
+ end
433
+
434
+ next if conf_mappable_accessorials.present? && conf_mappable_accessorials.keys.any?(accessorial)
435
+ next if conf_unquotable_accessorials.present? && conf_unquotable_accessorials.any?(accessorial)
436
+
437
+ unserviceable_accessorials << accessorial
438
+ end
439
+
440
+ if unserviceable_accessorials.present?
441
+ raise FreightKit::UnserviceableAccessorialsError.new(accessorials: unserviceable_accessorials)
442
+ end
443
+
444
+ true
445
+ end
446
+
447
+ protected
448
+
449
+ include ActiveUtils::RequiresParameters
450
+ include ActiveUtils::PostsData
451
+
452
+ # Use after building the request to save for later inspection.
453
+ # @return [void]
454
+ def save_request(request)
455
+ @last_request = request
456
+ end
457
+
458
+ # Calculates a timestamp that corresponds a given number of business days in the future
459
+ #
460
+ # @param days [Integer] The number of business days from now.
461
+ # @return [Time] A timestamp, the provided number of business days in the future.
462
+ def timestamp_from_business_day(days)
463
+ raise ArgumentError, 'days must be an Integer' unless days.is_a?(Integer)
464
+
465
+ date = Time.current.utc + days.days
466
+
467
+ date += 2.days if date.saturday?
468
+ date += 1.day if date.sunday?
469
+
470
+ date
471
+ end
472
+ end
473
+ end
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ module FreightKit
4
+ module Carriers
5
+ extend self
6
+
7
+ attr_reader :registered
8
+
9
+ @registered = []
10
+
11
+ def register(class_name, autoload_require)
12
+ FreightKit.autoload(class_name, autoload_require)
13
+ registered << class_name
14
+ end
15
+
16
+ def all
17
+ FreightKit::Carriers.registered.map { |name| FreightKit.const_get(name) }
18
+ end
19
+
20
+ def find(name)
21
+ all.find { |c| c.name.downcase == name.to_s.downcase } or raise NameError, "unknown carrier #{name}"
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ module FreightKit
4
+ # Contact is the abstract base class for all contacts.
5
+ #
6
+ # @!attribute company_name [String] Company name.
7
+ # @!attribute department_name [String] Department name (like "Shipping Dept").
8
+ # @!attribute email [String] Email.
9
+ # @!attribute fax [String] E164 formatted fax number.
10
+ # @!attribute name [String] Name of person.
11
+ # @!attribute phone [String] E164 formatted phone number.
12
+ class Contact < Model
13
+ attr_accessor :company_name, :department, :email, :fax, :name, :phone
14
+
15
+ alias_method :company, :company_name
16
+ end
17
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module FreightKit
4
+ class Error < ActiveUtils::ActiveUtilsError; end
5
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module FreightKit
4
+ class DocumentNotFoundError < FreightKit::Error; end
5
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module FreightKit
4
+ class ExpiredCredentialsError < FreightKit::InvalidCredentialsError; end
5
+ end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ module FreightKit
4
+ class HTTPError < FreightKit::Error
5
+ attr_reader :body, :code
6
+
7
+ def initialize(body:, code:)
8
+ @body = body
9
+ @code = code
10
+
11
+ super(message)
12
+ end
13
+
14
+ def message
15
+ @message ||= ''.tap do |builder|
16
+ builder << "HTTP #{@code}"
17
+ builder << ":\n#{@body}" if @body.present?
18
+ end
19
+ end
20
+
21
+ def to_hash
22
+ @to_hash ||= { code: @http.code, headers: @http.headers, body: @http.body }
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module FreightKit
4
+ class InvalidCredentialsError < FreightKit::Error; end
5
+ end
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ module FreightKit
4
+ class ResponseError < FreightKit::Error
5
+ attr_reader :response
6
+
7
+ def initialize(response = nil)
8
+ if response.is_a?(Response)
9
+ super(response.message)
10
+ @response = response
11
+ else
12
+ super(response)
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module FreightKit
4
+ class ShipmentNotFoundError < FreightKit::Error; end
5
+ end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ module FreightKit
4
+ class UnserviceableAccessorialsError < FreightKit::UnserviceableError
5
+ attr_reader :accessorials
6
+
7
+ def initialize(accessorials:)
8
+ @accessorials = accessorials
9
+
10
+ super(message)
11
+ end
12
+
13
+ def message
14
+ @message ||= "Unable to service #{@accessorials.map { |accessorial| accessorial.to_s.gsub("_", " ") }.join(", ")}"
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module FreightKit
4
+ class UnserviceableError < FreightKit::Error; end
5
+ end
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'freight_kit/errors/http_error'
4
+ require 'freight_kit/errors/invalid_credentials_error'
5
+ require 'freight_kit/errors/expired_credentials_error'
6
+ require 'freight_kit/errors/document_not_found_error'
7
+ require 'freight_kit/errors/shipment_not_found_error'
8
+ require 'freight_kit/errors/response_error'
9
+ require 'freight_kit/errors/unserviceable_error'
10
+ require 'freight_kit/errors/unserviceable_accessorials_error'
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ module FreightKit
4
+ class Model
5
+ include ActiveModel::AttributeAssignment
6
+ include ActiveModel::Validations
7
+
8
+ def initialize(attributes = {})
9
+ assign_attributes(attributes)
10
+ end
11
+
12
+ def attributes
13
+ instance_values.with_indifferent_access
14
+ end
15
+ alias_method :to_hash, :attributes
16
+ end
17
+ end