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,342 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module FreightKit
|
|
4
|
+
class SEFL < FreightKit::Carrier
|
|
5
|
+
class << self
|
|
6
|
+
def find_rates_with_declared_value?
|
|
7
|
+
true
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def maximum_height
|
|
11
|
+
Measured::Length.new(105, :inches)
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def maximum_weight
|
|
15
|
+
Measured::Weight.new(10_000, :pounds)
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def minimum_length_for_overlength_fees
|
|
19
|
+
Measured::Length.new(8, :feet)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def overlength_fees_require_tariff?
|
|
23
|
+
false
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def required_credential_types
|
|
27
|
+
%i[api]
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def requirements
|
|
31
|
+
%i[credentials]
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
REACTIVE_FREIGHT_CARRIER = true
|
|
36
|
+
|
|
37
|
+
class << self
|
|
38
|
+
attr_reader :name, :scac
|
|
39
|
+
end
|
|
40
|
+
@name = 'Southeastern Freight Lines'
|
|
41
|
+
@scac = 'SEFL'
|
|
42
|
+
|
|
43
|
+
JSON_HEADERS = {
|
|
44
|
+
Accept: 'application/json',
|
|
45
|
+
charset: 'utf-8',
|
|
46
|
+
'Content-Type' => 'application/x-www-form-urlencoded'
|
|
47
|
+
}.freeze
|
|
48
|
+
|
|
49
|
+
# Documents
|
|
50
|
+
|
|
51
|
+
# Rates
|
|
52
|
+
def find_rates(shipment:)
|
|
53
|
+
begin
|
|
54
|
+
validate_packages(shipment.packages)
|
|
55
|
+
rescue UnserviceableError => e
|
|
56
|
+
return RateResponse.new(error: e)
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
request = build_rate_request(shipment:)
|
|
60
|
+
parse_rate_response(shipment:, response: commit(request))
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# Tracking
|
|
64
|
+
|
|
65
|
+
protected
|
|
66
|
+
|
|
67
|
+
def build_url(action)
|
|
68
|
+
"#{base_url}#{@conf.dig(:api, :endpoints, action)}"
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def base_url
|
|
72
|
+
"https://#{@conf.dig(:api, :domain)}"
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def auth_header
|
|
76
|
+
api_credentials = fetch_credential(:api)
|
|
77
|
+
auth = Base64.strict_encode64("#{api_credentials.username}:#{api_credentials.password}")
|
|
78
|
+
|
|
79
|
+
{ Authorization: "Basic #{auth}" }
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def build_request(action, options = {})
|
|
83
|
+
headers = JSON_HEADERS
|
|
84
|
+
headers = headers.merge(auth_header)
|
|
85
|
+
headers = headers.merge(options[:headers]) if options[:headers].present?
|
|
86
|
+
body = URI.encode_www_form(options[:body]) if options[:body].present?
|
|
87
|
+
|
|
88
|
+
request = {
|
|
89
|
+
url: options[:url].presence || build_url(action),
|
|
90
|
+
headers:,
|
|
91
|
+
method: @conf.dig(:api, :methods, action),
|
|
92
|
+
body:
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
save_request(request)
|
|
96
|
+
request
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def commit(request)
|
|
100
|
+
url = request[:url]
|
|
101
|
+
headers = request[:headers]
|
|
102
|
+
method = request[:method]
|
|
103
|
+
body = request[:body]
|
|
104
|
+
|
|
105
|
+
case method
|
|
106
|
+
when :post
|
|
107
|
+
HTTParty.post(url, headers:, body:)
|
|
108
|
+
else
|
|
109
|
+
HTTParty.get(url, headers:)
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
# Documents
|
|
114
|
+
|
|
115
|
+
# Rates
|
|
116
|
+
def build_rate_request(shipment:)
|
|
117
|
+
accessorials = []
|
|
118
|
+
|
|
119
|
+
if shipment.accessorials.any?
|
|
120
|
+
serviceable_accessorials?(shipment.accessorials)
|
|
121
|
+
|
|
122
|
+
shipment
|
|
123
|
+
.accessorials
|
|
124
|
+
.reject { |accessorial| conf.dig(:accessorials, :unquotable).include?(accessorial) }
|
|
125
|
+
.each do |shipment_accessorial|
|
|
126
|
+
conf_accessorial = conf.dig(:accessorials, :mappable, shipment_accessorial)
|
|
127
|
+
|
|
128
|
+
case conf_accessorial
|
|
129
|
+
when Array then accessorials += conf_accessorial
|
|
130
|
+
when String then accessorials << conf_accessorial
|
|
131
|
+
end
|
|
132
|
+
end
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
longest_dimension = shipment.packages.map { |p| [p.width(:inches), p.length(:inches)].max }.max.ceil
|
|
136
|
+
accessorials << 'chkOD' if longest_dimension >= 96
|
|
137
|
+
|
|
138
|
+
accessorials.uniq!
|
|
139
|
+
|
|
140
|
+
pickup_on = Date.current
|
|
141
|
+
shipment_description = shipment.packages.map(&:description).reject(&:blank?).uniq.join(', ')
|
|
142
|
+
shipment_description = 'Freight All Kinds' if shipment_description.blank?
|
|
143
|
+
|
|
144
|
+
api_credentials = fetch_credential(:api)
|
|
145
|
+
|
|
146
|
+
body = {
|
|
147
|
+
allowSpot: longest_dimension >= 120 ? 'Y' : 'N',
|
|
148
|
+
CustomerAccount: api_credentials.account.to_i.to_s.rjust(9, '0'),
|
|
149
|
+
CustomerCity: customer_location.city,
|
|
150
|
+
CustomerName: customer_location.contact.company_name,
|
|
151
|
+
CustomerState: customer_location.province,
|
|
152
|
+
CustomerStreet: customer_location.address1,
|
|
153
|
+
CustomerZip: customer_location.postal_code,
|
|
154
|
+
Description: shipment_description,
|
|
155
|
+
DestCountry: 'U',
|
|
156
|
+
DestinationCity: shipment.destination.city,
|
|
157
|
+
DestinationState: shipment.destination.province,
|
|
158
|
+
DestinationZip: shipment.destination.postal_code,
|
|
159
|
+
DimsOption: 'I',
|
|
160
|
+
EmailAddress: customer_location.contact.email,
|
|
161
|
+
Option: 'T',
|
|
162
|
+
OrigCountry: 'U',
|
|
163
|
+
OriginCity: shipment.origin.city,
|
|
164
|
+
OriginState: shipment.origin.province,
|
|
165
|
+
OriginZip: shipment.origin.postal_code,
|
|
166
|
+
PickupDay: pickup_on.strftime('%_d'),
|
|
167
|
+
PickupMonth: pickup_on.strftime('%_m'),
|
|
168
|
+
PickupYear: pickup_on.strftime('%Y'),
|
|
169
|
+
rateXML: 'Y',
|
|
170
|
+
returnX: 'Y',
|
|
171
|
+
Terms: 'P'
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
declared_value = if shipment.declared_value_cents.blank?
|
|
175
|
+
0
|
|
176
|
+
else
|
|
177
|
+
(shipment.declared_value_cents.to_f / 100).ceil
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
if declared_value.positive?
|
|
181
|
+
body.deep_merge!({
|
|
182
|
+
chkIN: 'on',
|
|
183
|
+
FVInsuranceAmount: format('%.2f', declared_value)
|
|
184
|
+
})
|
|
185
|
+
end
|
|
186
|
+
body.deep_merge!({ ODLength: longest_dimension, ODLengthUnit: 'I' }) if longest_dimension >= 96
|
|
187
|
+
|
|
188
|
+
cubic_ft_required = shipment.destination.province.upcase == 'PR'
|
|
189
|
+
|
|
190
|
+
i = 0
|
|
191
|
+
shipment.packages.each do |package|
|
|
192
|
+
package.quantity.times do
|
|
193
|
+
i += 1
|
|
194
|
+
|
|
195
|
+
body = body.deep_merge({ "Class#{i}": package.freight_class.to_s.sub('.', '').to_i })
|
|
196
|
+
body = body.deep_merge({ "Description#{i}": package.description || 'Freight' })
|
|
197
|
+
body = body.deep_merge({ "PieceLength#{i}": package.length(:in).ceil })
|
|
198
|
+
body = body.deep_merge({ "PieceWidth#{i}": package.width(:in).ceil })
|
|
199
|
+
body = body.deep_merge({ "PieceHeight#{i}": package.height(:in).ceil })
|
|
200
|
+
body = body.deep_merge({ "Weight#{i}": package.pounds(:each).ceil })
|
|
201
|
+
|
|
202
|
+
body = body.deep_merge({ "CubicFt#{i}": package.cubic_ft(:each) }) if cubic_ft_required
|
|
203
|
+
end
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
if accessorials.any?
|
|
207
|
+
body[:accessorial] = 'on'
|
|
208
|
+
|
|
209
|
+
accessorials.each { |accessorial| body[accessorial] = 'on' }
|
|
210
|
+
end
|
|
211
|
+
|
|
212
|
+
request = build_request(:rates, body:)
|
|
213
|
+
save_request(request)
|
|
214
|
+
request
|
|
215
|
+
end
|
|
216
|
+
|
|
217
|
+
def parse_rate_response(shipment:, response:, tries: 0)
|
|
218
|
+
rate_response = RateResponse.new(request: last_request, response:)
|
|
219
|
+
|
|
220
|
+
# Used begin rescue block's retry instead.
|
|
221
|
+
# if tries > 10
|
|
222
|
+
# rate_response.error = ResponseError.new("Timeout after #{tries * 5} seconds")
|
|
223
|
+
# return rate_response
|
|
224
|
+
# end
|
|
225
|
+
|
|
226
|
+
if response.body.blank?
|
|
227
|
+
rate_response.error = InvalidCredentialsError if response.code == 401
|
|
228
|
+
|
|
229
|
+
rate_response.error = ResponseError.new('Unknown response') if rate_response.error.blank?
|
|
230
|
+
return rate_response
|
|
231
|
+
end
|
|
232
|
+
|
|
233
|
+
begin
|
|
234
|
+
response = JSON.parse(response.body)
|
|
235
|
+
rescue JSON::ParserError
|
|
236
|
+
sleep(5)
|
|
237
|
+
if tries > 10
|
|
238
|
+
rate_response.error = ResponseError.new("Timeout after #{tries * 5} seconds")
|
|
239
|
+
return rate_response
|
|
240
|
+
end
|
|
241
|
+
|
|
242
|
+
tries += 1
|
|
243
|
+
retry
|
|
244
|
+
end
|
|
245
|
+
|
|
246
|
+
error = response['errorMessage']
|
|
247
|
+
|
|
248
|
+
if error.present?
|
|
249
|
+
if error.include?('one point must be directly serviced')
|
|
250
|
+
rate_response.error = UnserviceableError.new(error.sub(' by SEFL.', ''))
|
|
251
|
+
end
|
|
252
|
+
|
|
253
|
+
rate_response.error = ResponseError.new(error) if rate_response.error.blank?
|
|
254
|
+
return rate_response
|
|
255
|
+
end
|
|
256
|
+
|
|
257
|
+
url = response['detailQuoteLocation'].gsub('\\', '')
|
|
258
|
+
request = build_request(:get_rate, url:)
|
|
259
|
+
|
|
260
|
+
tries = 0
|
|
261
|
+
|
|
262
|
+
until tries > 10
|
|
263
|
+
save_request(request)
|
|
264
|
+
response = commit(request)
|
|
265
|
+
|
|
266
|
+
if response.body.blank?
|
|
267
|
+
rate_response.error = InvalidCredentialsError if response.code == 401
|
|
268
|
+
|
|
269
|
+
rate_response.error = ResponseError.new('Unknown response') if rate_response.error.blank?
|
|
270
|
+
return rate_response
|
|
271
|
+
end
|
|
272
|
+
|
|
273
|
+
response = JSON.parse(response.body)
|
|
274
|
+
|
|
275
|
+
if response.blank?
|
|
276
|
+
rate_response.error = ResponseError.new('Unknown response')
|
|
277
|
+
return rate_response
|
|
278
|
+
end
|
|
279
|
+
|
|
280
|
+
error = response['errorMessage']
|
|
281
|
+
|
|
282
|
+
if error.present?
|
|
283
|
+
if error.downcase.include?('not yet been processed')
|
|
284
|
+
sleep(5)
|
|
285
|
+
tries += 1
|
|
286
|
+
next
|
|
287
|
+
else
|
|
288
|
+
rate_response.error = ResponseError.new(error)
|
|
289
|
+
return rate_response
|
|
290
|
+
end
|
|
291
|
+
end
|
|
292
|
+
|
|
293
|
+
tries = 50
|
|
294
|
+
end
|
|
295
|
+
|
|
296
|
+
if response['rateQuote'].blank?
|
|
297
|
+
rate_response.error = ResponseError.new('Cost is empty')
|
|
298
|
+
return rate_response
|
|
299
|
+
end
|
|
300
|
+
|
|
301
|
+
estimate_reference = response['quoteNumber']
|
|
302
|
+
transit_days = response['transitTime'].to_i
|
|
303
|
+
|
|
304
|
+
details = response['details']
|
|
305
|
+
prices = []
|
|
306
|
+
|
|
307
|
+
# return details
|
|
308
|
+
|
|
309
|
+
details.each do |detail|
|
|
310
|
+
next if detail['typeCharge'].include?('TTL') || detail['typeCharge'].include?('NFC')
|
|
311
|
+
|
|
312
|
+
cents = detail['charges'].squish
|
|
313
|
+
cents = cents.blank? ? 0 : (cents.to_f * 100).to_i
|
|
314
|
+
next if cents.zero?
|
|
315
|
+
|
|
316
|
+
description = detail['description'].squish
|
|
317
|
+
|
|
318
|
+
cents *= -1 if detail['description'].include?('DISCOUNT')
|
|
319
|
+
|
|
320
|
+
prices << Price.new(blame: :api, cents:, description:)
|
|
321
|
+
end
|
|
322
|
+
|
|
323
|
+
rate = Rate.new(
|
|
324
|
+
carrier: self,
|
|
325
|
+
carrier_name: self.class.name,
|
|
326
|
+
currency: 'USD',
|
|
327
|
+
estimate_reference:,
|
|
328
|
+
scac: self.class.scac.upcase,
|
|
329
|
+
service_name: :standard,
|
|
330
|
+
shipment:,
|
|
331
|
+
prices:,
|
|
332
|
+
transit_days:,
|
|
333
|
+
with_excessive_length_fees: @conf.dig(:attributes, :rates, :with_excessive_length_fees),
|
|
334
|
+
)
|
|
335
|
+
|
|
336
|
+
rate_response.rates = [rate]
|
|
337
|
+
rate_response
|
|
338
|
+
end
|
|
339
|
+
|
|
340
|
+
# Tracking
|
|
341
|
+
end
|
|
342
|
+
end
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module FreightKit
|
|
4
|
+
class TOTL < CarrierLogistics
|
|
5
|
+
class << self
|
|
6
|
+
def maximum_height
|
|
7
|
+
Measured::Length.new(105, :inches)
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def maximum_weight
|
|
11
|
+
Measured::Weight.new(10_000, :pounds)
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def minimum_length_for_overlength_fees
|
|
15
|
+
Measured::Length.new(40, :inches)
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def overlength_fees_require_tariff?
|
|
19
|
+
false
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def pickup_number_is_tracking_number?
|
|
23
|
+
true
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def required_credential_types
|
|
27
|
+
%i[api]
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def requirements
|
|
31
|
+
%i[credentials]
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
REACTIVE_FREIGHT_CARRIER = true
|
|
36
|
+
|
|
37
|
+
class << self
|
|
38
|
+
attr_reader :name, :scac
|
|
39
|
+
end
|
|
40
|
+
@name = 'Total Transportation'
|
|
41
|
+
@scac = 'TOTL'
|
|
42
|
+
|
|
43
|
+
# Documents
|
|
44
|
+
|
|
45
|
+
# Pickups
|
|
46
|
+
|
|
47
|
+
# Rates
|
|
48
|
+
|
|
49
|
+
# Tracking
|
|
50
|
+
|
|
51
|
+
# protected
|
|
52
|
+
|
|
53
|
+
# Documents
|
|
54
|
+
|
|
55
|
+
# Rates
|
|
56
|
+
|
|
57
|
+
def parse_rate_response(shipment:, response:)
|
|
58
|
+
rate_response = RateResponse.new(request: last_request, response:)
|
|
59
|
+
|
|
60
|
+
if response.blank?
|
|
61
|
+
rate_response.error = ResponseError.new('Unknown response')
|
|
62
|
+
return rate_response
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
if response.is_a?(String) && response.include?('WebSpeed error')
|
|
66
|
+
rate_response.error = ResponseError.new('API Error: Temporary error (CarrierLogistics WebSpeed error)')
|
|
67
|
+
return rate_response
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
error = response.dig('error', 'errormessage')
|
|
71
|
+
|
|
72
|
+
if error.present?
|
|
73
|
+
if error.downcase.include?('invalid username/password')
|
|
74
|
+
rate_response.error = InvalidCredentialsError.new
|
|
75
|
+
return rate_response
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
if error.downcase.include?('is not available') || error.downcase.include?('out of the serviceable area')
|
|
79
|
+
rate_response.error = UnserviceableError.new
|
|
80
|
+
return rate_response
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
rate_response.error = ResponseError.new("API Error: #{error}")
|
|
84
|
+
return rate_response
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
if response.dig('ratequote', 'quotetotal').blank?
|
|
88
|
+
rate_response.error = ResponseError.new('Cost is blank')
|
|
89
|
+
return rate_response
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
total_cents = parse_amount(response.dig('ratequote', 'quotetotal'))
|
|
93
|
+
|
|
94
|
+
transit_days = response.dig('ratequote', 'busdays').to_i
|
|
95
|
+
estimate_reference = response.dig('ratequote', 'quotenumber')
|
|
96
|
+
|
|
97
|
+
ratequote_lines = response.dig('ratequote', 'ratequoteline')
|
|
98
|
+
|
|
99
|
+
prices = []
|
|
100
|
+
ratequote_lines.each do |ratequote_line|
|
|
101
|
+
next if ratequote_line['chrg'].blank?
|
|
102
|
+
next if ratequote_line['chargedesc'] == 'FREIGHT'
|
|
103
|
+
|
|
104
|
+
cents = parse_amount(ratequote_line['chrg'])
|
|
105
|
+
next if cents.zero?
|
|
106
|
+
|
|
107
|
+
prices << Price.new(
|
|
108
|
+
blame: :api,
|
|
109
|
+
cents:,
|
|
110
|
+
description: ratequote_line_description(ratequote_line),
|
|
111
|
+
)
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
prices = [
|
|
115
|
+
Price.new(
|
|
116
|
+
blame: :api,
|
|
117
|
+
cents: total_cents - prices.sum(&:cents),
|
|
118
|
+
description: 'Freight',
|
|
119
|
+
),
|
|
120
|
+
] + prices
|
|
121
|
+
|
|
122
|
+
# Carrier-specific pricing structure
|
|
123
|
+
oversized_pallets_cents = 0
|
|
124
|
+
|
|
125
|
+
shipment.packages.each do |package|
|
|
126
|
+
short_side, long_side = nil
|
|
127
|
+
if package.length(:in).present? && package.width(:in).present? && package.height(:in).present?
|
|
128
|
+
long_side = [package.length(:in), package.width(:in)].max
|
|
129
|
+
short_side = [package.length(:in), package.width(:in)].min
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
next unless short_side &&
|
|
133
|
+
long_side &&
|
|
134
|
+
package.height(:in) &&
|
|
135
|
+
(
|
|
136
|
+
short_side > 40 ||
|
|
137
|
+
long_side > 48 ||
|
|
138
|
+
package.height(:in) > 84
|
|
139
|
+
)
|
|
140
|
+
|
|
141
|
+
oversized_pallets_cents += 1500
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
if oversized_pallets_cents.nonzero?
|
|
145
|
+
prices << Price.new(
|
|
146
|
+
blame: :library,
|
|
147
|
+
cents: oversized_pallets_cents,
|
|
148
|
+
description: 'Overlength fees',
|
|
149
|
+
)
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
RateResponse.new(
|
|
153
|
+
rates: [
|
|
154
|
+
Rate.new(
|
|
155
|
+
carrier: self,
|
|
156
|
+
carrier_name: self.class.name,
|
|
157
|
+
currency: 'USD',
|
|
158
|
+
estimate_reference:,
|
|
159
|
+
scac: self.class.scac.upcase,
|
|
160
|
+
service_name: :standard,
|
|
161
|
+
shipment:,
|
|
162
|
+
prices:,
|
|
163
|
+
transit_days:,
|
|
164
|
+
with_excessive_length_fees: @conf.dig(:attributes, :rates, :with_excessive_length_fees),
|
|
165
|
+
),
|
|
166
|
+
],
|
|
167
|
+
request: last_request,
|
|
168
|
+
response:,
|
|
169
|
+
)
|
|
170
|
+
end
|
|
171
|
+
end
|
|
172
|
+
end
|