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,528 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module FreightKit
|
|
4
|
+
class TheGreatInformationFactory < Platform
|
|
5
|
+
class << self
|
|
6
|
+
def required_credential_types
|
|
7
|
+
%i[api]
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def requirements
|
|
11
|
+
%i[credentials tariff]
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def overlength_fees_require_tariff?
|
|
15
|
+
true
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
REACTIVE_FREIGHT_PLATFORM = true
|
|
20
|
+
|
|
21
|
+
include FreightKit::Rateable
|
|
22
|
+
include FreightKit::Trackable
|
|
23
|
+
include FreightKit::Pickupable
|
|
24
|
+
|
|
25
|
+
protected
|
|
26
|
+
|
|
27
|
+
def wrap_request(request)
|
|
28
|
+
{ 'arg0' => request }
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def build_soap_header
|
|
32
|
+
api_credentials = fetch_credential(:api)
|
|
33
|
+
|
|
34
|
+
{ username: api_credentials.username, password: api_credentials.password }
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def commit(action, request)
|
|
38
|
+
client_args = {
|
|
39
|
+
wsdl: build_url(action),
|
|
40
|
+
convert_request_keys_to: :upcase,
|
|
41
|
+
env_namespace: :soapenv
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
call_args = {
|
|
45
|
+
headers: { 'SOAPAction' => '""' },
|
|
46
|
+
soap_action: false,
|
|
47
|
+
message: request
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
::FreightKit::SoapClient.new(
|
|
51
|
+
carrier: self,
|
|
52
|
+
action:,
|
|
53
|
+
client_args:,
|
|
54
|
+
call_args:,
|
|
55
|
+
soap_operation: @conf.dig(:api, :actions, action),
|
|
56
|
+
).call
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def parse_api_date(date)
|
|
60
|
+
return if date.blank?
|
|
61
|
+
|
|
62
|
+
local_date = ::Date.strptime(date, '%m/%d/%Y')
|
|
63
|
+
::FreightKit::DateTime.new(local_date:)
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def parse_api_date_time(date_time, location)
|
|
67
|
+
return if date_time.blank?
|
|
68
|
+
|
|
69
|
+
format = date_time.include?('-') ? '%Y-%m-%d %H:%M' : '%m/%d/%Y %H:%M'
|
|
70
|
+
|
|
71
|
+
local_date_time = ::Time.strptime(date_time, format).to_fs(:db)
|
|
72
|
+
::FreightKit::DateTime.new(local_date_time:, location:)
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def build_url(action)
|
|
76
|
+
scheme = @conf.dig(:api, :use_ssl, action) ? 'https://' : 'http://'
|
|
77
|
+
domain = @conf.dig(:api, :domains, action).presence || @conf.dig(:api, :domain)
|
|
78
|
+
port = @conf.dig(:api, :ports, action)
|
|
79
|
+
return [scheme, domain, @conf.dig(:api, :endpoints, action)].join unless port
|
|
80
|
+
|
|
81
|
+
"#{scheme}#{domain}:#{port}#{@conf.dig(:api, :endpoints, action)}"
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def strip_date(str)
|
|
85
|
+
str ? str.split(/[A|P]M /)[1] : nil
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
# Rates
|
|
89
|
+
|
|
90
|
+
def build_calculated_accessorials(packages)
|
|
91
|
+
[]
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def build_rate_request(shipment:)
|
|
95
|
+
accessorials = []
|
|
96
|
+
|
|
97
|
+
if shipment.accessorials.present?
|
|
98
|
+
serviceable_accessorials?(shipment.accessorials)
|
|
99
|
+
|
|
100
|
+
accessorials = shipment
|
|
101
|
+
.accessorials
|
|
102
|
+
.select { |accessorial| @conf.dig(:accessorials, :unserviceable).exclude?(accessorial) }
|
|
103
|
+
.map { |accessorial| @conf.dig(:accessorials, :mappable, accessorial) }
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
accessorials += build_calculated_accessorials(shipment.packages)
|
|
107
|
+
accessorials.uniq!
|
|
108
|
+
|
|
109
|
+
item = shipment.packages.map do |package|
|
|
110
|
+
{
|
|
111
|
+
_class: package.freight_class,
|
|
112
|
+
description: (package.description || 'Freight')[..8].upcase,
|
|
113
|
+
haz: (package.hazmat? ? 'Y' : ''),
|
|
114
|
+
pallets: (package.packaging.pallet? ? package.quantity : 0),
|
|
115
|
+
pieces: package.quantity,
|
|
116
|
+
weight: package.pounds(:total).ceil
|
|
117
|
+
}
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
request = {
|
|
121
|
+
securityinfo: build_soap_header,
|
|
122
|
+
quote: {
|
|
123
|
+
iam: 'D', # S for shipper, C for consignee, D for third party
|
|
124
|
+
shipper: {
|
|
125
|
+
city: shipment.origin.city.upcase,
|
|
126
|
+
state: shipment.origin.province.upcase,
|
|
127
|
+
zip: shipment.origin.postal_code.gsub(/\s+/, '').upcase
|
|
128
|
+
},
|
|
129
|
+
consignee: {
|
|
130
|
+
city: shipment.destination.city.upcase,
|
|
131
|
+
state: shipment.destination.province.upcase,
|
|
132
|
+
zip: shipment.destination.postal_code.gsub(/\s+/, '').upcase
|
|
133
|
+
},
|
|
134
|
+
accessorialcount: accessorials.count,
|
|
135
|
+
accessorial: accessorials.map { |code| { code: } },
|
|
136
|
+
ppdcol: 'P', # Prepaid
|
|
137
|
+
itemcount: shipment.packages.sum(&:quantity),
|
|
138
|
+
item:
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
request = wrap_request(request)
|
|
143
|
+
save_request(request)
|
|
144
|
+
|
|
145
|
+
request
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
def rate_item_description(rate_item)
|
|
149
|
+
description = rate_item[:description] || ''
|
|
150
|
+
description = description.gsub('-', '')
|
|
151
|
+
description = description.squish
|
|
152
|
+
description = description.sub('disc.on', 'discount on')
|
|
153
|
+
description = description.capitalize
|
|
154
|
+
description = description.sub('zip code', 'ZIP code')
|
|
155
|
+
description.sub('Zip code', 'ZIP code')
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
def parse_rate_response(shipment:, response:)
|
|
159
|
+
rate_response = RateResponse.new(request: last_request, response:)
|
|
160
|
+
|
|
161
|
+
if response.blank?
|
|
162
|
+
rate_response.error = ResponseError.new('Unknown response')
|
|
163
|
+
return rate_response
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
error_code = response.dig(:getquote_response, :return, :rating, :errorcode)
|
|
167
|
+
if error_code
|
|
168
|
+
rate_response.error = parse_error_response(error_code)
|
|
169
|
+
return rate_response
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
total_cents = response.dig(:getquote_response, :return, :rating, :amount)
|
|
173
|
+
|
|
174
|
+
raise FreightKit::ResponseError, 'API Error: Cost is empty' if total_cents.blank?
|
|
175
|
+
|
|
176
|
+
rate_items = response.dig(:getquote_response, :return, :rateitem)
|
|
177
|
+
total_cents = (total_cents.to_f * 100).to_i
|
|
178
|
+
|
|
179
|
+
prices = []
|
|
180
|
+
|
|
181
|
+
# Confusing API sometimes returns lines of freight with high costs and then later includes includes lines that
|
|
182
|
+
# override the high cost without adding a discount, etc
|
|
183
|
+
rate_items.each do |rate_item|
|
|
184
|
+
next if ['Sub Total', 'GrandTotal'].include?(rate_item[:acccode])
|
|
185
|
+
|
|
186
|
+
# Exclude lines that are just the packages repeated back to us
|
|
187
|
+
next unless rate_item[:pallets] == '0' && rate_item[:pieces] == '0'
|
|
188
|
+
|
|
189
|
+
cents = (rate_item[:amount].to_f * 100).to_i
|
|
190
|
+
description = rate_item_description(rate_item)
|
|
191
|
+
|
|
192
|
+
prices << Price.new(blame: :api, cents:, description:)
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
# Since we expected the low-cost overriding lines earlier, we need to handle situations where those lines do not
|
|
196
|
+
# appear
|
|
197
|
+
if prices.sum(&:cents) < total_cents
|
|
198
|
+
prices = [
|
|
199
|
+
Price.new(
|
|
200
|
+
blame: :api,
|
|
201
|
+
cents: total_cents - prices.sum(&:cents),
|
|
202
|
+
description: 'Freight',
|
|
203
|
+
),
|
|
204
|
+
] + prices
|
|
205
|
+
end
|
|
206
|
+
|
|
207
|
+
if self.class.overlength_fees_require_tariff?
|
|
208
|
+
shipment.packages.each do |package|
|
|
209
|
+
cents = overlength_fee(tariff, package)
|
|
210
|
+
next unless cents.positive?
|
|
211
|
+
|
|
212
|
+
prices << Price.new(
|
|
213
|
+
blame: :tariff,
|
|
214
|
+
cents:,
|
|
215
|
+
description: 'Overlength fee',
|
|
216
|
+
)
|
|
217
|
+
end
|
|
218
|
+
end
|
|
219
|
+
|
|
220
|
+
transit_days = response.dig(
|
|
221
|
+
:getquote_response,
|
|
222
|
+
:return,
|
|
223
|
+
:service,
|
|
224
|
+
:days,
|
|
225
|
+
).to_i
|
|
226
|
+
|
|
227
|
+
# Calculate real transit time based on information we have about the destination service days
|
|
228
|
+
%i[mon tue wed thu fri].each do |weekday|
|
|
229
|
+
transit_days += 1 if response.dig(:getquote_response, :return, :service, :destination, weekday) == 'N'
|
|
230
|
+
end
|
|
231
|
+
|
|
232
|
+
estimate_reference = response.dig(
|
|
233
|
+
:getquote_response,
|
|
234
|
+
:return,
|
|
235
|
+
:rating,
|
|
236
|
+
:quotenumber,
|
|
237
|
+
)
|
|
238
|
+
|
|
239
|
+
rate = Rate.new(
|
|
240
|
+
carrier_name: self.class.name,
|
|
241
|
+
carrier: self,
|
|
242
|
+
currency: 'USD',
|
|
243
|
+
estimate_reference:,
|
|
244
|
+
prices:,
|
|
245
|
+
scac: self.class.scac.upcase,
|
|
246
|
+
service_name: :standard,
|
|
247
|
+
shipment:,
|
|
248
|
+
transit_days:,
|
|
249
|
+
with_excessive_length_fees: @conf.dig(:attributes, :rates, :with_excessive_length_fees),
|
|
250
|
+
)
|
|
251
|
+
|
|
252
|
+
rate_response.rates = [rate]
|
|
253
|
+
rate_response
|
|
254
|
+
end
|
|
255
|
+
|
|
256
|
+
# Tracking
|
|
257
|
+
|
|
258
|
+
def build_tracking_request(tracking_number)
|
|
259
|
+
request = { pronumber: tracking_number, securityinfo: build_soap_header }
|
|
260
|
+
|
|
261
|
+
request = wrap_request(request)
|
|
262
|
+
save_request(request)
|
|
263
|
+
|
|
264
|
+
request
|
|
265
|
+
end
|
|
266
|
+
|
|
267
|
+
def parse_location(code)
|
|
268
|
+
country = ActiveUtils::Country.find('USA')
|
|
269
|
+
return Location.new(country:) unless code
|
|
270
|
+
|
|
271
|
+
location = @conf.dig(:events, :locations, code.to_sym)
|
|
272
|
+
|
|
273
|
+
if location
|
|
274
|
+
Location.new(
|
|
275
|
+
city: location[:city],
|
|
276
|
+
province: location[:state],
|
|
277
|
+
country:,
|
|
278
|
+
)
|
|
279
|
+
else
|
|
280
|
+
Location.new(city: code, country:)
|
|
281
|
+
end
|
|
282
|
+
end
|
|
283
|
+
|
|
284
|
+
def parse_error_response(error_code)
|
|
285
|
+
case error_code
|
|
286
|
+
when 'BADUSRPWD' then InvalidCredentialsError.new
|
|
287
|
+
when 'NOSVC' then UnserviceableError.new('Origin or destination has no service available')
|
|
288
|
+
when 'BADCONZIP' then UnserviceableError.new('Invalid destination ZIP code')
|
|
289
|
+
else
|
|
290
|
+
ResponseError.new("API error code #{error_code}")
|
|
291
|
+
end
|
|
292
|
+
end
|
|
293
|
+
|
|
294
|
+
def build_location(city, province)
|
|
295
|
+
Location.new(city: city.titleize, province: province.upcase, country: ActiveUtils::Country.find('USA'))
|
|
296
|
+
end
|
|
297
|
+
|
|
298
|
+
def parse_tracking_response(response)
|
|
299
|
+
tracking_response = TrackingResponse.new(carrier: self, request: last_request, response:)
|
|
300
|
+
mapped_response = response.dig(:tracktrace_response, :return, :currentstatus)
|
|
301
|
+
|
|
302
|
+
if mapped_response[:errorcode]
|
|
303
|
+
tracking_response.error = parse_error_response(mapped_response[:errorcode])
|
|
304
|
+
return tracking_response
|
|
305
|
+
end
|
|
306
|
+
|
|
307
|
+
receiver_location = build_location(
|
|
308
|
+
mapped_response.dig(:consignee, :city),
|
|
309
|
+
mapped_response.dig(:consignee, :state),
|
|
310
|
+
)
|
|
311
|
+
shipper_location = build_location(mapped_response.dig(:shipper, :city), mapped_response.dig(:shipper, :state))
|
|
312
|
+
|
|
313
|
+
actual_delivery_date = mapped_response[:deliverydate]
|
|
314
|
+
|
|
315
|
+
if actual_delivery_date.present?
|
|
316
|
+
comment = mapped_response[:status].downcase
|
|
317
|
+
|
|
318
|
+
if comment.starts_with?('delivered')
|
|
319
|
+
api_date = comment.downcase.split('signed')[0].split('on')[1].strip.sub('at ', '')
|
|
320
|
+
actual_delivery_date = parse_api_date(api_date)
|
|
321
|
+
end
|
|
322
|
+
end
|
|
323
|
+
|
|
324
|
+
shipment_events = []
|
|
325
|
+
|
|
326
|
+
ship_time = parse_api_date(mapped_response[:shipdate])
|
|
327
|
+
# Leave this open for modification later
|
|
328
|
+
picked_up_event = ShipmentEvent.new(location: shipper_location, date_time: ship_time, type_code: :picked_up)
|
|
329
|
+
|
|
330
|
+
scheduled_delivery_date = parse_api_date(mapped_response[:estdeliverydate])
|
|
331
|
+
tracking_number = response.dig(:tracktrace_response, :return, :pronumber)
|
|
332
|
+
|
|
333
|
+
api_events = response.dig(:tracktrace_response, :return, :history)
|
|
334
|
+
api_events = [api_events] if api_events.is_a?(Hash)
|
|
335
|
+
|
|
336
|
+
last_location = nil
|
|
337
|
+
|
|
338
|
+
api_events.each_with_index do |api_event, index|
|
|
339
|
+
next if api_event[:description].blank?
|
|
340
|
+
|
|
341
|
+
event = nil
|
|
342
|
+
@conf.dig(:events, :types).each do |key, val|
|
|
343
|
+
next if api_event[:description].downcase.exclude?(val)
|
|
344
|
+
|
|
345
|
+
event = key
|
|
346
|
+
break
|
|
347
|
+
end
|
|
348
|
+
next if event.blank?
|
|
349
|
+
|
|
350
|
+
location = if api_event[:location].blank?
|
|
351
|
+
case event
|
|
352
|
+
when :departed then last_location
|
|
353
|
+
when :picked_up, :pickup_information_sent_to_carrier then shipper_location
|
|
354
|
+
when :delivered, :out_for_delivery then receiver_location
|
|
355
|
+
end
|
|
356
|
+
else
|
|
357
|
+
parse_location(api_event[:location])
|
|
358
|
+
end
|
|
359
|
+
|
|
360
|
+
api_date_time = "#{api_event[:date]} #{api_event[:time]}"
|
|
361
|
+
date_time = api_event[:date].present? ? parse_api_date_time(api_date_time, location) : nil
|
|
362
|
+
|
|
363
|
+
case event
|
|
364
|
+
when :arrived_at_terminal
|
|
365
|
+
# Duplicate event occurs without location data from API
|
|
366
|
+
break if api_event[:location].blank?
|
|
367
|
+
when :delivered
|
|
368
|
+
actual_delivery_date = date_time
|
|
369
|
+
when :out_for_delivery
|
|
370
|
+
# Do not consider out for delivery when out for delivery and interlined dates match
|
|
371
|
+
next_api_event = api_events[index + 1]
|
|
372
|
+
|
|
373
|
+
break if next_api_event.blank?
|
|
374
|
+
|
|
375
|
+
if next_api_event[:description].include?('INTERLINE') && next_api_event[:date] == api_event[:date]
|
|
376
|
+
shipment_events << ShipmentEvent.new(date_time:, location:, type_code: :departed)
|
|
377
|
+
next
|
|
378
|
+
end
|
|
379
|
+
when :pickup_information_sent_to_carrier
|
|
380
|
+
# Pickup event appears after carrier information sent, let's fix that
|
|
381
|
+
picked_up_event.date_time = date_time.dup
|
|
382
|
+
end
|
|
383
|
+
|
|
384
|
+
last_location = location
|
|
385
|
+
|
|
386
|
+
shipment_events << ShipmentEvent.new(date_time:, location:, type_code: event)
|
|
387
|
+
end
|
|
388
|
+
|
|
389
|
+
shipment_events << picked_up_event
|
|
390
|
+
|
|
391
|
+
if shipment_events.collect(&:date_time).none?(nil)
|
|
392
|
+
shipment_events = shipment_events.sort_by do |shipment_event|
|
|
393
|
+
d = shipment_event.date_time
|
|
394
|
+
d&.local_date_time || d.date_time_with_zone&.to_fs(:db) || d.local_date&.to_fs(:db)
|
|
395
|
+
end
|
|
396
|
+
end
|
|
397
|
+
|
|
398
|
+
status = shipment_events.last&.type_code
|
|
399
|
+
|
|
400
|
+
# Workarounds for false status on certain events when timestamps are in wrong order
|
|
401
|
+
status = :out_for_delivery if shipment_events.find do |shipment_event|
|
|
402
|
+
shipment_event.type_code == :out_for_delivery
|
|
403
|
+
end
|
|
404
|
+
status = :delivered if shipment_events.find { |shipment_event| shipment_event.type_code == :delivered }
|
|
405
|
+
|
|
406
|
+
tracking_response.assign_attributes(
|
|
407
|
+
actual_delivery_date:,
|
|
408
|
+
destination: receiver_location,
|
|
409
|
+
origin: shipper_location,
|
|
410
|
+
scheduled_delivery_date:,
|
|
411
|
+
ship_time:,
|
|
412
|
+
shipment_events:,
|
|
413
|
+
status:,
|
|
414
|
+
tracking_number:,
|
|
415
|
+
)
|
|
416
|
+
|
|
417
|
+
tracking_response
|
|
418
|
+
end
|
|
419
|
+
|
|
420
|
+
def parse_pickup_response(response)
|
|
421
|
+
pickup_response = PickupResponse.new(request: last_request, response:)
|
|
422
|
+
|
|
423
|
+
result = response.dig(:requestpickup_response, :return, :results)
|
|
424
|
+
if result[:errorcode]
|
|
425
|
+
pickup_response.error = FreightKit::ResponseError.new("API Error: #{result[:errorcode]}")
|
|
426
|
+
return pickup_response
|
|
427
|
+
end
|
|
428
|
+
|
|
429
|
+
pickup_number = result[:pickupnumber]
|
|
430
|
+
|
|
431
|
+
if pickup_number == '0'
|
|
432
|
+
pickup_response.error = FreightKit::ResponseError.new('Unknown Error')
|
|
433
|
+
return pickup_response
|
|
434
|
+
end
|
|
435
|
+
|
|
436
|
+
pickup_response.pickup_number = pickup_number
|
|
437
|
+
pickup_response
|
|
438
|
+
end
|
|
439
|
+
|
|
440
|
+
def build_pickup_request(
|
|
441
|
+
delivery_from:,
|
|
442
|
+
delivery_to:,
|
|
443
|
+
dispatcher:,
|
|
444
|
+
pickup_from:,
|
|
445
|
+
pickup_to:,
|
|
446
|
+
scac:,
|
|
447
|
+
service:,
|
|
448
|
+
shipment:
|
|
449
|
+
)
|
|
450
|
+
|
|
451
|
+
shipper_phone = shipment.origin.contact.phone.gsub(/\s+/, '').gsub(/[()-+.]/, '')
|
|
452
|
+
shipper_phone = shipper_phone[1..] if shipper_phone.length == 11
|
|
453
|
+
|
|
454
|
+
request = {
|
|
455
|
+
securityinfo: build_soap_header,
|
|
456
|
+
shipperinfo: {
|
|
457
|
+
name: shipment.origin.contact.company_name,
|
|
458
|
+
contact: shipment.origin.contact.name,
|
|
459
|
+
phonenumber: shipper_phone,
|
|
460
|
+
address1: shipment.origin.address1,
|
|
461
|
+
address2: shipment.origin.address2,
|
|
462
|
+
city: shipment.origin.city,
|
|
463
|
+
state: shipment.origin.province,
|
|
464
|
+
ReadyDate: pickup_from.strftime('%m/%d/%Y'),
|
|
465
|
+
ReadyTime: pickup_from.strftime('%H%M').to_i,
|
|
466
|
+
CloseTime: pickup_to.strftime('%H%M').to_i,
|
|
467
|
+
zip: shipment.origin.postal_code,
|
|
468
|
+
SpecialInstructions: ''
|
|
469
|
+
},
|
|
470
|
+
ShipmentCount: 1,
|
|
471
|
+
shipments: [
|
|
472
|
+
{
|
|
473
|
+
DestZip: shipment.destination.postal_code,
|
|
474
|
+
Pieces: shipment.packages.sum(&:quantity),
|
|
475
|
+
Pallets: shipment.packages.select { |p| p.packaging.pallet? }.sum(&:quantity),
|
|
476
|
+
Weight: shipment.packages.sum { |p| p.pounds(:total).ceil },
|
|
477
|
+
HAZ: shipment.packages.any?(&:hazmat?) ? 'Y' : 'N',
|
|
478
|
+
dblStack: 'N',
|
|
479
|
+
SortSeg: 'N',
|
|
480
|
+
Pro: shipment.pro,
|
|
481
|
+
Liftgate: shipment.accessorials.include?(:liftgate_pickup) ? 'Y' : 'N'
|
|
482
|
+
},
|
|
483
|
+
]
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
request = wrap_request(request)
|
|
487
|
+
save_request(request)
|
|
488
|
+
|
|
489
|
+
request
|
|
490
|
+
end
|
|
491
|
+
|
|
492
|
+
def parse_document_response(type, tracking_number)
|
|
493
|
+
base_url = build_url(type)
|
|
494
|
+
website_credentials = fetch_credential(:api)
|
|
495
|
+
query_parameter = "&username=#{website_credentials.username}&" \
|
|
496
|
+
"password=#{website_credentials.password}&" \
|
|
497
|
+
"pronumber=#{tracking_number}&" \
|
|
498
|
+
'format=PDF'
|
|
499
|
+
|
|
500
|
+
url = [base_url, query_parameter].join
|
|
501
|
+
|
|
502
|
+
response = HTTParty.get(url)
|
|
503
|
+
|
|
504
|
+
image = response.deep_symbolize_keys.dig(:ImageRequest, :Image)
|
|
505
|
+
image = image.last if image.is_a?(Array)
|
|
506
|
+
# ImageRequest[:Image] sometimes return an Array and both images are identical
|
|
507
|
+
|
|
508
|
+
document_response = DocumentResponse.new(request: url)
|
|
509
|
+
|
|
510
|
+
unless image
|
|
511
|
+
document_response.error = DocumentNotFoundError.new
|
|
512
|
+
return document_response
|
|
513
|
+
end
|
|
514
|
+
|
|
515
|
+
base64_document_data = image.dig(:ImageData, :__content__)
|
|
516
|
+
|
|
517
|
+
unless base64_document_data
|
|
518
|
+
document_response.error = DocumentNotFoundError.new
|
|
519
|
+
return document_response
|
|
520
|
+
end
|
|
521
|
+
|
|
522
|
+
decoded_pdf_data = Base64.decode64(base64_document_data)
|
|
523
|
+
document_response.assign_attributes(content_type: 'application/pdf', data: decoded_pdf_data)
|
|
524
|
+
|
|
525
|
+
document_response
|
|
526
|
+
end
|
|
527
|
+
end
|
|
528
|
+
end
|
data/lib/freight_kit.rb
CHANGED
|
@@ -4,7 +4,6 @@ require 'active_model'
|
|
|
4
4
|
require 'active_support/all'
|
|
5
5
|
require 'active_utils'
|
|
6
6
|
require 'business_time'
|
|
7
|
-
require 'cgi'
|
|
8
7
|
require 'httparty'
|
|
9
8
|
require 'measured'
|
|
10
9
|
require 'mimemagic'
|
|
@@ -35,6 +34,26 @@ loader = Zeitwerk::Loader.for_gem
|
|
|
35
34
|
loader.collapse("#{__dir__}/freight_kit/errors")
|
|
36
35
|
loader.collapse("#{__dir__}/freight_kit/models")
|
|
37
36
|
|
|
37
|
+
# Carriers, platforms, helpers, and api_clients define top-level constants
|
|
38
|
+
# under FreightKit:: (e.g. FreightKit::ABFS, FreightKit::Rateable) rather
|
|
39
|
+
# than nested under the directory name, so they're loaded manually via the
|
|
40
|
+
# explicit requires below.
|
|
41
|
+
loader.ignore("#{__dir__}/freight_kit/api_clients.rb")
|
|
42
|
+
loader.ignore("#{__dir__}/freight_kit/api_clients")
|
|
43
|
+
loader.ignore("#{__dir__}/freight_kit/carriers.rb")
|
|
44
|
+
loader.ignore("#{__dir__}/freight_kit/carriers")
|
|
45
|
+
loader.ignore("#{__dir__}/freight_kit/helpers.rb")
|
|
46
|
+
loader.ignore("#{__dir__}/freight_kit/helpers")
|
|
47
|
+
loader.ignore("#{__dir__}/freight_kit/platforms.rb")
|
|
48
|
+
loader.ignore("#{__dir__}/freight_kit/platforms")
|
|
49
|
+
|
|
38
50
|
loader.inflector = FreightKit::Inflector.new
|
|
39
51
|
|
|
40
52
|
loader.setup
|
|
53
|
+
|
|
54
|
+
require 'rmagick'
|
|
55
|
+
|
|
56
|
+
require 'freight_kit/api_clients'
|
|
57
|
+
require 'freight_kit/helpers'
|
|
58
|
+
require 'freight_kit/platforms'
|
|
59
|
+
require 'freight_kit/carriers'
|