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.
- checksums.yaml +7 -0
- data/Gemfile +5 -0
- data/Gemfile.lock +201 -0
- data/MIT-LICENSE +31 -0
- data/README.md +153 -0
- data/VERSION +1 -0
- data/accessorial_symbols.txt +95 -0
- data/freight_kit.gemspec +58 -0
- data/lib/freight_kit/carrier.rb +473 -0
- data/lib/freight_kit/carriers.rb +24 -0
- data/lib/freight_kit/contact.rb +17 -0
- data/lib/freight_kit/error.rb +5 -0
- data/lib/freight_kit/errors/document_not_found_error.rb +5 -0
- data/lib/freight_kit/errors/expired_credentials_error.rb +5 -0
- data/lib/freight_kit/errors/http_error.rb +25 -0
- data/lib/freight_kit/errors/invalid_credentials_error.rb +5 -0
- data/lib/freight_kit/errors/response_error.rb +16 -0
- data/lib/freight_kit/errors/shipment_not_found_error.rb +5 -0
- data/lib/freight_kit/errors/unserviceable_accessorials_error.rb +17 -0
- data/lib/freight_kit/errors/unserviceable_error.rb +5 -0
- data/lib/freight_kit/errors.rb +10 -0
- data/lib/freight_kit/model.rb +17 -0
- data/lib/freight_kit/models/credential.rb +117 -0
- data/lib/freight_kit/models/date_time.rb +37 -0
- data/lib/freight_kit/models/document_response.rb +17 -0
- data/lib/freight_kit/models/label.rb +13 -0
- data/lib/freight_kit/models/location.rb +108 -0
- data/lib/freight_kit/models/pickup_response.rb +19 -0
- data/lib/freight_kit/models/price.rb +38 -0
- data/lib/freight_kit/models/rate.rb +81 -0
- data/lib/freight_kit/models/rate_response.rb +15 -0
- data/lib/freight_kit/models/response.rb +21 -0
- data/lib/freight_kit/models/shipment.rb +66 -0
- data/lib/freight_kit/models/shipment_event.rb +38 -0
- data/lib/freight_kit/models/tracking_response.rb +75 -0
- data/lib/freight_kit/models.rb +17 -0
- data/lib/freight_kit/package.rb +313 -0
- data/lib/freight_kit/package_item.rb +65 -0
- data/lib/freight_kit/packaging.rb +52 -0
- data/lib/freight_kit/platform.rb +36 -0
- data/lib/freight_kit/shipment_packer.rb +116 -0
- data/lib/freight_kit/tariff.rb +29 -0
- data/lib/freight_kit/version.rb +5 -0
- data/lib/freight_kit.rb +34 -0
- data/service_type_symbols.txt +4 -0
- data/shipment_event_symbols.txt +17 -0
- 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,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,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,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,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
|