freight_kit 0.1.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 (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