freight_kit 0.1.15 → 0.1.16
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 +4 -4
- data/Gemfile.lock +96 -43
- data/README.md +28 -6
- data/VERSION +1 -1
- data/configuration/carriers/abfs.yml +124 -0
- data/configuration/carriers/btvp.yml +84 -0
- data/configuration/carriers/ccyq.yml +121 -0
- data/configuration/carriers/clni.yml +113 -0
- data/configuration/carriers/cnwy.yml +113 -0
- data/configuration/carriers/ctbv.yml +117 -0
- data/configuration/carriers/dcha.yml +105 -0
- data/configuration/carriers/dlds.yml +111 -0
- data/configuration/carriers/dphe.yml +130 -0
- data/configuration/carriers/drrq.yml +131 -0
- data/configuration/carriers/fcsy.yml +102 -0
- data/configuration/carriers/fwda.yml +137 -0
- data/configuration/carriers/jfj_transportation.yml +2 -0
- data/configuration/carriers/mtvl.yml +12 -0
- data/configuration/carriers/numk.yml +14 -0
- data/configuration/carriers/otcl.yml +124 -0
- data/configuration/carriers/pens.yml +22 -0
- data/configuration/carriers/rdfs.yml +142 -0
- data/configuration/carriers/saia.yml +129 -0
- data/configuration/carriers/sefl.yml +115 -0
- data/configuration/carriers/totl.yml +111 -0
- data/configuration/carriers/tqyl.yml +28 -0
- data/configuration/carriers/wrds.yml +20 -0
- data/configuration/platforms/carrier_logistics.yml +25 -0
- data/configuration/platforms/next.yml +12 -0
- data/configuration/platforms/the_great_information_factory.yml +122 -0
- data/freight_kit.gemspec +9 -7
- data/lib/freight_kit/api_clients/soap_client.rb +70 -0
- data/lib/freight_kit/api_clients.rb +3 -0
- data/lib/freight_kit/carriers/abfs.rb +421 -0
- data/lib/freight_kit/carriers/btvp.rb +29 -0
- data/lib/freight_kit/carriers/ccyq.rb +317 -0
- data/lib/freight_kit/carriers/clni.rb +396 -0
- data/lib/freight_kit/carriers/cnwy.rb +327 -0
- data/lib/freight_kit/carriers/ctbv.rb +53 -0
- data/lib/freight_kit/carriers/dcha.rb +76 -0
- data/lib/freight_kit/carriers/dlds.rb +49 -0
- data/lib/freight_kit/carriers/dphe.rb +474 -0
- data/lib/freight_kit/carriers/drrq.rb +580 -0
- data/lib/freight_kit/carriers/fcsy.rb +57 -0
- data/lib/freight_kit/carriers/fwda.rb +744 -0
- data/lib/freight_kit/carriers/jfj_transportation.rb +13 -0
- data/lib/freight_kit/carriers/mtvl.rb +34 -0
- data/lib/freight_kit/carriers/numk.rb +58 -0
- data/lib/freight_kit/carriers/otcl.rb +528 -0
- data/lib/freight_kit/carriers/pens.rb +204 -0
- data/lib/freight_kit/carriers/rdfs.rb +521 -0
- data/lib/freight_kit/carriers/saia.rb +438 -0
- data/lib/freight_kit/carriers/sefl.rb +342 -0
- data/lib/freight_kit/carriers/totl.rb +172 -0
- data/lib/freight_kit/carriers/tqyl.rb +339 -0
- data/lib/freight_kit/carriers/wrds.rb +246 -0
- data/lib/freight_kit/carriers.rb +26 -0
- data/lib/freight_kit/helpers/documentable.rb +13 -0
- data/lib/freight_kit/helpers/pickupable.rb +39 -0
- data/lib/freight_kit/helpers/rateable.rb +28 -0
- data/lib/freight_kit/helpers/trackable.rb +25 -0
- data/lib/freight_kit/helpers.rb +6 -0
- data/lib/freight_kit/platforms/carrier_logistics.rb +450 -0
- data/lib/freight_kit/platforms/next.rb +101 -0
- data/lib/freight_kit/platforms/the_great_information_factory.rb +528 -0
- data/lib/freight_kit/platforms.rb +5 -0
- data/lib/freight_kit.rb +20 -1
- metadata +94 -14
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module FreightKit
|
|
4
|
+
module Trackable
|
|
5
|
+
def find_tracking_info(tracking_number, *)
|
|
6
|
+
request = build_tracking_request(tracking_number)
|
|
7
|
+
begin
|
|
8
|
+
# For SOAP APIs, the :action parameter is required
|
|
9
|
+
response = commit(:track, request) if method(:commit).parameters.count == 2
|
|
10
|
+
response ||= commit(request)
|
|
11
|
+
rescue StandardError => e
|
|
12
|
+
return TrackingResponse.new(error: e, request:)
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
return response if response.is_a?(TrackingResponse)
|
|
16
|
+
|
|
17
|
+
if method(:parse_tracking_response).parameters.count == 1
|
|
18
|
+
parse_tracking_response(response)
|
|
19
|
+
else
|
|
20
|
+
# Carrier Logistics requires tracking number argument
|
|
21
|
+
parse_tracking_response(tracking_number, response:)
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
@@ -0,0 +1,450 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module FreightKit
|
|
4
|
+
class CarrierLogistics < Platform
|
|
5
|
+
class << self
|
|
6
|
+
def required_credential_types
|
|
7
|
+
%i[api]
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def requirements
|
|
11
|
+
return %i[credentials tariff] if overlength_fees_require_tariff?
|
|
12
|
+
|
|
13
|
+
%i[credentials]
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
REACTIVE_FREIGHT_PLATFORM = true
|
|
18
|
+
|
|
19
|
+
EXPIRED_CREDENTIALS_MESSAGES = [
|
|
20
|
+
'Your password has expired',
|
|
21
|
+
].freeze
|
|
22
|
+
INVALID_CREDENTIALS_MESSAGES = [
|
|
23
|
+
'Unable to log in',
|
|
24
|
+
'Your Username or Password is Incorrect',
|
|
25
|
+
].freeze
|
|
26
|
+
|
|
27
|
+
include FreightKit::Trackable
|
|
28
|
+
include FreightKit::Rateable
|
|
29
|
+
|
|
30
|
+
# Documents
|
|
31
|
+
|
|
32
|
+
def pod(tracking_number)
|
|
33
|
+
query = build_tracking_request(tracking_number)
|
|
34
|
+
response = commit(:track, query)
|
|
35
|
+
parse_document_response(response, :pod)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def scanned_bol(tracking_number)
|
|
39
|
+
query = build_tracking_request(tracking_number)
|
|
40
|
+
response = commit(:track, query)
|
|
41
|
+
parse_document_response(response, :bol)
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# protected
|
|
45
|
+
|
|
46
|
+
def build_url(action, query:)
|
|
47
|
+
scheme = @conf.dig(:api, :use_ssl, action) ? 'https://' : 'http://'
|
|
48
|
+
|
|
49
|
+
uri = URI.parse("#{scheme}#{@conf.dig(:api, :domain)}#{@conf.dig(:api, :endpoints, action)}")
|
|
50
|
+
uri.query = query.to_query
|
|
51
|
+
url = uri.to_s
|
|
52
|
+
return url if url.exclude?('@CARRIER_CODE@')
|
|
53
|
+
|
|
54
|
+
url.sub('@CARRIER_CODE@', @conf.dig(:api, :carrier_code))
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def commit(action, query)
|
|
58
|
+
url = build_url(action, query:)
|
|
59
|
+
save_request(url)
|
|
60
|
+
|
|
61
|
+
HTTParty.get(url, logger: Logger.new($stdout))
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def map_response_errors(response, not_found_error: DocumentNotFoundError)
|
|
65
|
+
return ResponseError.new('Unknown response') if response.blank?
|
|
66
|
+
|
|
67
|
+
webspeed_error = (response.is_a?(String) || response.is_a?(HTTParty::Response)) &&
|
|
68
|
+
response.include?('WebSpeed error')
|
|
69
|
+
return ResponseError.new('Temporary error (WebSpeed error)') if webspeed_error
|
|
70
|
+
|
|
71
|
+
return if response.code == 200
|
|
72
|
+
|
|
73
|
+
response.code == 400 ? not_found_error.new : ResponseError.new("HTTP #{response.code}")
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
# Documents
|
|
77
|
+
|
|
78
|
+
def parse_document_response(tracking_response, document_type)
|
|
79
|
+
document_response = DocumentResponse.new
|
|
80
|
+
document_response.error = map_response_errors(tracking_response)
|
|
81
|
+
return document_response if document_response.error.present?
|
|
82
|
+
|
|
83
|
+
tracking_response.deep_symbolize_keys!
|
|
84
|
+
|
|
85
|
+
image_type_code = case document_type
|
|
86
|
+
when :bol then 'B'
|
|
87
|
+
when :pod then 'P'
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
api_images = tracking_response.dig(:protrace, :images, :image)
|
|
91
|
+
api_images = [api_images] if api_images.is_a?(Hash)
|
|
92
|
+
|
|
93
|
+
image = api_images&.find { |image| image[:imagetypecode] == image_type_code }
|
|
94
|
+
url = image.blank? ? nil : (image[:directurl].presence || image[:imageurl])
|
|
95
|
+
|
|
96
|
+
if url.blank?
|
|
97
|
+
document_response.error = DocumentNotFoundError.new
|
|
98
|
+
return document_response
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
begin
|
|
102
|
+
response = HTTParty.get(url)
|
|
103
|
+
rescue StandardError => e
|
|
104
|
+
document_response.error = e
|
|
105
|
+
return document_response
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
unless response.code == 200
|
|
109
|
+
document_response.error = DocumentNotFoundError.new
|
|
110
|
+
return document_response
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
document_response.assign_attributes(content_type: response.headers['content-type'], data: response.body)
|
|
114
|
+
document_response
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
# Tracking
|
|
118
|
+
|
|
119
|
+
def build_tracking_request(tracking_number)
|
|
120
|
+
api_credentials = fetch_credential(:api)
|
|
121
|
+
|
|
122
|
+
{ pronum: tracking_number, xmlpass: api_credentials.password, xmluser: api_credentials.username }
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
def parse_api_city_state(str)
|
|
126
|
+
return if str.blank?
|
|
127
|
+
|
|
128
|
+
city = str.split(', ')[0].titleize
|
|
129
|
+
province = str.split(', ')[1].upcase
|
|
130
|
+
|
|
131
|
+
if province == '*'
|
|
132
|
+
province = case city
|
|
133
|
+
when 'Albuquerque' then 'NM'
|
|
134
|
+
end
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
Location.new(
|
|
138
|
+
city:,
|
|
139
|
+
province:,
|
|
140
|
+
country: ActiveUtils::Country.find('USA'),
|
|
141
|
+
)
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
def parse_api_city_state_zip(str)
|
|
145
|
+
return if str.blank?
|
|
146
|
+
|
|
147
|
+
parts = str.split(', ')
|
|
148
|
+
|
|
149
|
+
Location.new(
|
|
150
|
+
city: parts.first.titleize,
|
|
151
|
+
province: parts.last.upcase,
|
|
152
|
+
country: ActiveUtils::Country.find('USA'),
|
|
153
|
+
)
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
def parse_api_date(date, location)
|
|
157
|
+
return if date.blank?
|
|
158
|
+
|
|
159
|
+
separator = ['?', '-'].find { |separator| date.include?(separator) }
|
|
160
|
+
return if separator.blank?
|
|
161
|
+
|
|
162
|
+
format = case date
|
|
163
|
+
when /^\d{4}#{separator}/
|
|
164
|
+
['%Y', '%m', '%d'].join(separator)
|
|
165
|
+
when /^\d{2}#{separator}/
|
|
166
|
+
['%m', '%d', '%Y'].join(separator)
|
|
167
|
+
end
|
|
168
|
+
return if format.blank?
|
|
169
|
+
|
|
170
|
+
local_date = ::Date.strptime(date, format)
|
|
171
|
+
::FreightKit::DateTime.new(local_date:, location:)
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
def parse_api_date_time(date_time, location)
|
|
175
|
+
return if date_time.blank?
|
|
176
|
+
|
|
177
|
+
local_date_time = ::Time.strptime(date_time, '%Y-%m-%d %H:%M').to_fs(:db)
|
|
178
|
+
::FreightKit::DateTime.new(local_date_time:, location:)
|
|
179
|
+
rescue Date::Error
|
|
180
|
+
raise if local_date_time.present?
|
|
181
|
+
|
|
182
|
+
parse_api_date(local_date_time, location)
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
def parse_tracking_response(tracking_number, response:)
|
|
186
|
+
tracking_response = TrackingResponse.new(carrier: self, request: last_request, response:)
|
|
187
|
+
tracking_response.error = map_response_errors(response, not_found_error: ShipmentNotFoundError)
|
|
188
|
+
return tracking_response if tracking_response.error.present?
|
|
189
|
+
|
|
190
|
+
response.deep_symbolize_keys!
|
|
191
|
+
|
|
192
|
+
api_events = response.dig(:protrace, :shiphists, :shiphist)
|
|
193
|
+
if api_events.blank?
|
|
194
|
+
tracking_response.error = ResponseError.new('Empty response')
|
|
195
|
+
return tracking_response
|
|
196
|
+
end
|
|
197
|
+
|
|
198
|
+
origin = Location.new(
|
|
199
|
+
address1: response.dig(:protrace, :shipaddr)&.titleize,
|
|
200
|
+
address2: response.dig(:protrace, :shipaddr2)&.titleize,
|
|
201
|
+
city: response.dig(:protrace, :origcity)&.titleize,
|
|
202
|
+
province: response.dig(:protrace, :origstate)&.upcase,
|
|
203
|
+
country: ActiveUtils::Country.find('USA'),
|
|
204
|
+
)
|
|
205
|
+
|
|
206
|
+
destination = Location.new(
|
|
207
|
+
address1: response.dig(:protrace, :consaddr)&.titleize,
|
|
208
|
+
address2: response.dig(:protrace, :consaddr2)&.titleize,
|
|
209
|
+
city: response.dig(:protrace, :destcity)&.titleize,
|
|
210
|
+
province: response.dig(:protrace, :deststate)&.upcase,
|
|
211
|
+
country: ActiveUtils::Country.find('USA'),
|
|
212
|
+
)
|
|
213
|
+
|
|
214
|
+
deldateiso = response.dig(:protrace, :deldateiso)
|
|
215
|
+
actual_delivery_date = (parse_api_date(deldateiso, destination) if deldateiso.present?)
|
|
216
|
+
|
|
217
|
+
estdeliverydateiso = response.dig(:protrace, :estdeliverydateiso)
|
|
218
|
+
estdeliverytimestart = response.dig(:protrace, :estdeliverytimestart)
|
|
219
|
+
estimated_delivery_date = if estdeliverydateiso.present? && estdeliverytimestart.present?
|
|
220
|
+
parse_api_date_time([estdeliverydateiso, estdeliverytimestart].join(' '), destination)
|
|
221
|
+
elsif estdeliverydateiso.present?
|
|
222
|
+
parse_api_date(estdeliverydateiso, destination)
|
|
223
|
+
end
|
|
224
|
+
|
|
225
|
+
scheduled_delivery_date = nil
|
|
226
|
+
ship_time = nil
|
|
227
|
+
|
|
228
|
+
api_events = response.dig(:protrace, :shiphists, :shiphist)
|
|
229
|
+
api_events = [api_events] if api_events.is_a?(Hash)
|
|
230
|
+
|
|
231
|
+
last_location = origin
|
|
232
|
+
|
|
233
|
+
shipment_events = api_events.reverse.map do |api_event|
|
|
234
|
+
hist_code = api_event[:histcode]&.downcase
|
|
235
|
+
next if hist_code.blank?
|
|
236
|
+
|
|
237
|
+
event = conf.dig(:events, :types).key(hist_code)
|
|
238
|
+
next if event.blank?
|
|
239
|
+
|
|
240
|
+
remarks = api_event[:histremarks]
|
|
241
|
+
|
|
242
|
+
location = if remarks.present? && remarks.match?(/, \w{2}/) # ends in state abbreviation
|
|
243
|
+
parse_api_city_state(api_event[:histremarks])
|
|
244
|
+
end
|
|
245
|
+
|
|
246
|
+
location ||= case event
|
|
247
|
+
when :delivered then destination
|
|
248
|
+
when :departed then last_location
|
|
249
|
+
when :picked_up, :pickup_scheduled then origin
|
|
250
|
+
end
|
|
251
|
+
|
|
252
|
+
date = api_event[:histdate]
|
|
253
|
+
time = api_event[:histtime]
|
|
254
|
+
# Some api_event[:histtime] returns a string with missing hours and minutes like ' : '
|
|
255
|
+
time = nil if (time =~ /\d/).blank?
|
|
256
|
+
|
|
257
|
+
date_time = if [date, time].all?(&:present?)
|
|
258
|
+
parse_api_date_time([date, time].compact.join(' '), location)
|
|
259
|
+
elsif date.present?
|
|
260
|
+
parse_api_date(date, location)
|
|
261
|
+
end
|
|
262
|
+
|
|
263
|
+
last_location = location
|
|
264
|
+
|
|
265
|
+
ShipmentEvent.new(location:, date_time:, type_code: event)
|
|
266
|
+
end
|
|
267
|
+
|
|
268
|
+
shipment_events.compact!
|
|
269
|
+
|
|
270
|
+
status = shipment_events.last&.type_code
|
|
271
|
+
|
|
272
|
+
tracking_response.assign_attributes(
|
|
273
|
+
actual_delivery_date:,
|
|
274
|
+
destination:,
|
|
275
|
+
estimated_delivery_date:,
|
|
276
|
+
origin:,
|
|
277
|
+
scheduled_delivery_date:,
|
|
278
|
+
ship_time:,
|
|
279
|
+
shipment_events:,
|
|
280
|
+
status:,
|
|
281
|
+
tracking_number:,
|
|
282
|
+
)
|
|
283
|
+
|
|
284
|
+
tracking_response
|
|
285
|
+
end
|
|
286
|
+
|
|
287
|
+
# Rates
|
|
288
|
+
|
|
289
|
+
def build_calculated_accessorials(shipment)
|
|
290
|
+
[]
|
|
291
|
+
end
|
|
292
|
+
|
|
293
|
+
def parse_amount(amount)
|
|
294
|
+
negative = amount.include?('-')
|
|
295
|
+
|
|
296
|
+
['$', ',', '-'].each do |char|
|
|
297
|
+
amount = amount.sub(char, '')
|
|
298
|
+
end
|
|
299
|
+
|
|
300
|
+
return 0 if amount.blank?
|
|
301
|
+
|
|
302
|
+
amount = (amount.to_f * 100).to_i
|
|
303
|
+
return amount unless negative
|
|
304
|
+
|
|
305
|
+
amount * -1
|
|
306
|
+
end
|
|
307
|
+
|
|
308
|
+
def ratequote_line_description(ratequote_line)
|
|
309
|
+
description = ratequote_line['chargedesc'] || ''
|
|
310
|
+
description = description.capitalize
|
|
311
|
+
|
|
312
|
+
code = ratequote_line['chargecode']&.upcase || ''
|
|
313
|
+
description = "#{description} (#{code})" if code.present?
|
|
314
|
+
|
|
315
|
+
description.squish
|
|
316
|
+
end
|
|
317
|
+
|
|
318
|
+
def build_rate_request(shipment:)
|
|
319
|
+
api_credentials = fetch_credential(:api)
|
|
320
|
+
|
|
321
|
+
query = {
|
|
322
|
+
xmlv: 'yes', # must be first
|
|
323
|
+
quotenumber: 'YES',
|
|
324
|
+
vdzip: shipment.destination.postal_code,
|
|
325
|
+
vozip: shipment.origin.postal_code,
|
|
326
|
+
xmlpass: api_credentials.password,
|
|
327
|
+
xmluser: api_credentials.username
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
i = 0
|
|
331
|
+
shipment.packages.each do |package|
|
|
332
|
+
i += 1 # API starts at 1 (not 0)
|
|
333
|
+
|
|
334
|
+
query["vclass[#{i}]"] = package.freight_class
|
|
335
|
+
query["wheight[#{i}]"] = package.height(:in).ceil
|
|
336
|
+
query["wlength[#{i}]"] = package.length(:in).ceil
|
|
337
|
+
query["wpallets[#{i}]"] = package.quantity
|
|
338
|
+
query["wpieces[#{i}]"] = package.quantity
|
|
339
|
+
query["wweight[#{i}]"] = package.pounds(:total).ceil
|
|
340
|
+
query["wwidth[#{i}]"] = package.width(:in).ceil
|
|
341
|
+
end
|
|
342
|
+
|
|
343
|
+
accessorials = []
|
|
344
|
+
|
|
345
|
+
if shipment.accessorials.present?
|
|
346
|
+
serviceable_accessorials?(shipment.accessorials)
|
|
347
|
+
|
|
348
|
+
shipment
|
|
349
|
+
.accessorials
|
|
350
|
+
.reject { |accessorial| conf.dig(:accessorials, :unquotable).include?(accessorial) }
|
|
351
|
+
.each do |shipment_accessorial|
|
|
352
|
+
conf_accessorial = conf.dig(:accessorials, :mappable, shipment_accessorial)
|
|
353
|
+
|
|
354
|
+
case conf_accessorial
|
|
355
|
+
when Array then accessorials += conf_accessorial
|
|
356
|
+
when String then accessorials << conf_accessorial
|
|
357
|
+
end
|
|
358
|
+
end
|
|
359
|
+
end
|
|
360
|
+
|
|
361
|
+
accessorials += build_calculated_accessorials(shipment)
|
|
362
|
+
|
|
363
|
+
accessorials.uniq.compact.each { |accessorial| query[accessorial] = 'Yes' } if accessorials.any?
|
|
364
|
+
|
|
365
|
+
query
|
|
366
|
+
end
|
|
367
|
+
|
|
368
|
+
def parse_rate_response(shipment:, response:)
|
|
369
|
+
rate_response = RateResponse.new(request: last_request, response:)
|
|
370
|
+
|
|
371
|
+
rate_response.error = map_response_errors(response)
|
|
372
|
+
return rate_response if rate_response.error.present?
|
|
373
|
+
|
|
374
|
+
error = response.dig('error', 'errormessage')
|
|
375
|
+
|
|
376
|
+
if error.present?
|
|
377
|
+
rate_response.error = InvalidCredentialsError.new if error.downcase.include?('invalid username/password')
|
|
378
|
+
|
|
379
|
+
if error.downcase.include?('is not available') || error.downcase.include?('out of the serviceable area')
|
|
380
|
+
rate_response.error = UnserviceableError.new(error)
|
|
381
|
+
end
|
|
382
|
+
|
|
383
|
+
rate_response.error = ResponseError.new(error) if rate_response.error.blank?
|
|
384
|
+
|
|
385
|
+
return rate_response
|
|
386
|
+
end
|
|
387
|
+
|
|
388
|
+
if response.dig('ratequote', 'quotetotal').blank?
|
|
389
|
+
rate_response.error = ResponseError.new('Cost is blank')
|
|
390
|
+
return rate_response
|
|
391
|
+
end
|
|
392
|
+
|
|
393
|
+
total_cents = parse_amount(response.dig('ratequote', 'quotetotal'))
|
|
394
|
+
|
|
395
|
+
transit_days = response.dig('ratequote', 'busdays').to_i
|
|
396
|
+
estimate_reference = response.dig('ratequote', 'quotenumber')
|
|
397
|
+
|
|
398
|
+
ratequote_lines = response.dig('ratequote', 'ratequoteline')
|
|
399
|
+
prices = []
|
|
400
|
+
|
|
401
|
+
ratequote_lines.each do |ratequote_line|
|
|
402
|
+
next if ratequote_line['chrg'].blank?
|
|
403
|
+
next if ratequote_line['chargedesc'] == 'FREIGHT'
|
|
404
|
+
|
|
405
|
+
cents = parse_amount(ratequote_line['chrg'])
|
|
406
|
+
next if cents.zero?
|
|
407
|
+
|
|
408
|
+
prices << Price.new(
|
|
409
|
+
blame: :api,
|
|
410
|
+
cents:,
|
|
411
|
+
description: ratequote_line_description(ratequote_line),
|
|
412
|
+
)
|
|
413
|
+
end
|
|
414
|
+
|
|
415
|
+
prices = [
|
|
416
|
+
Price.new(
|
|
417
|
+
blame: :api,
|
|
418
|
+
cents: total_cents - prices.sum(&:cents),
|
|
419
|
+
description: 'Freight',
|
|
420
|
+
),
|
|
421
|
+
] + prices
|
|
422
|
+
|
|
423
|
+
if self.class.overlength_fees_require_tariff?
|
|
424
|
+
cents = 0
|
|
425
|
+
|
|
426
|
+
shipment.packages.each do |package|
|
|
427
|
+
cents += overlength_fee(tariff, package)
|
|
428
|
+
end
|
|
429
|
+
|
|
430
|
+
prices << Price.new(blame: :tariff, cents:, description: 'Overlength fees') if cents.nonzero?
|
|
431
|
+
end
|
|
432
|
+
|
|
433
|
+
rate = Rate.new(
|
|
434
|
+
carrier: self,
|
|
435
|
+
carrier_name: self.class.name,
|
|
436
|
+
currency: 'USD',
|
|
437
|
+
estimate_reference:,
|
|
438
|
+
scac: self.class.scac.upcase,
|
|
439
|
+
service_name: :standard,
|
|
440
|
+
shipment:,
|
|
441
|
+
prices:,
|
|
442
|
+
transit_days:,
|
|
443
|
+
with_excessive_length_fees: @conf.dig(:attributes, :rates, :with_excessive_length_fees),
|
|
444
|
+
)
|
|
445
|
+
|
|
446
|
+
rate_response.rates = [rate]
|
|
447
|
+
rate_response
|
|
448
|
+
end
|
|
449
|
+
end
|
|
450
|
+
end
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module FreightKit
|
|
4
|
+
class Next < Platform
|
|
5
|
+
class << self
|
|
6
|
+
def required_credential_types
|
|
7
|
+
%i[api]
|
|
8
|
+
end
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
REACTIVE_FREIGHT_PLATFORM = true
|
|
12
|
+
|
|
13
|
+
JSON_HEADERS = {
|
|
14
|
+
Accept: 'application/json',
|
|
15
|
+
'Content-Type': 'application/json',
|
|
16
|
+
charset: 'utf-8'
|
|
17
|
+
}.freeze
|
|
18
|
+
|
|
19
|
+
# Documents
|
|
20
|
+
|
|
21
|
+
# Rates
|
|
22
|
+
|
|
23
|
+
def show(id)
|
|
24
|
+
request = build_request(:show, params: "/#{id}")
|
|
25
|
+
commit(request)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# Tracking
|
|
29
|
+
|
|
30
|
+
# protected
|
|
31
|
+
|
|
32
|
+
def build_url(action, options = {})
|
|
33
|
+
url = ''.dup
|
|
34
|
+
url += "#{base_url}#{@conf.dig(:api, :scopes, options[:scope])}#{@conf.dig(:api, :endpoints, action)}"
|
|
35
|
+
url = url.sub(@conf.dig(:api, :scopes, options[:scope]), '') if action == :authenticate
|
|
36
|
+
url += options[:params] if options[:params].present?
|
|
37
|
+
url
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def build_request(action, options = {})
|
|
41
|
+
headers = JSON_HEADERS
|
|
42
|
+
headers = headers.merge(options[:headers]) if options[:headers].present?
|
|
43
|
+
body = options[:body].to_json if options[:body].present?
|
|
44
|
+
|
|
45
|
+
unless action == :authenticate
|
|
46
|
+
set_auth_token
|
|
47
|
+
headers = headers.merge(token)
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
request = {
|
|
51
|
+
url: build_url(action, options),
|
|
52
|
+
headers:,
|
|
53
|
+
method: @conf.dig(:api, :methods, action),
|
|
54
|
+
body:
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
save_request(request)
|
|
58
|
+
request
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def commit(request)
|
|
62
|
+
url = request[:url]
|
|
63
|
+
headers = request[:headers]
|
|
64
|
+
method = request[:method]
|
|
65
|
+
body = request[:body]
|
|
66
|
+
|
|
67
|
+
response = case method
|
|
68
|
+
when :post
|
|
69
|
+
HTTParty.post(url, headers:, body:)
|
|
70
|
+
else
|
|
71
|
+
HTTParty.get(url, headers:)
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
JSON.parse(response.body) if response&.body
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def base_url
|
|
78
|
+
"https://#{@conf.dig(:api, :domain)}#{@conf.dig(:api, :prefix)}#{@conf.dig(:api, :scope, @options[:scope])}"
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def set_auth_token
|
|
82
|
+
return @auth_token if @auth_token.present?
|
|
83
|
+
|
|
84
|
+
api_credentials = fetch_credential(:api)
|
|
85
|
+
|
|
86
|
+
request = build_request(
|
|
87
|
+
:authenticate,
|
|
88
|
+
body: { email: api_credentials.username, password: api_credentials.password },
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
response = commit(request)
|
|
92
|
+
@auth_token = response['auth_token']
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def token
|
|
96
|
+
{ Authorization: "Bearer #{@auth_token}" }
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
# Show
|
|
100
|
+
end
|
|
101
|
+
end
|