reactive_freight 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/Gemfile +5 -0
- data/MIT-LICENSE +20 -0
- data/README.md +166 -0
- data/Rakefile +8 -0
- data/accessorial_symbols.txt +95 -0
- data/lib/reactive_freight.rb +21 -0
- data/lib/reactive_freight/carrier.rb +62 -0
- data/lib/reactive_freight/carriers.rb +18 -0
- data/lib/reactive_freight/carriers/btvp.rb +384 -0
- data/lib/reactive_freight/carriers/clni.rb +59 -0
- data/lib/reactive_freight/carriers/ctbv.rb +35 -0
- data/lib/reactive_freight/carriers/dphe.rb +296 -0
- data/lib/reactive_freight/carriers/drrq.rb +303 -0
- data/lib/reactive_freight/carriers/fcsy.rb +24 -0
- data/lib/reactive_freight/carriers/fwda.rb +243 -0
- data/lib/reactive_freight/carriers/jfj_transportation.rb +11 -0
- data/lib/reactive_freight/carriers/pens.rb +135 -0
- data/lib/reactive_freight/carriers/rdfs.rb +320 -0
- data/lib/reactive_freight/carriers/saia.rb +336 -0
- data/lib/reactive_freight/carriers/sefl.rb +234 -0
- data/lib/reactive_freight/carriers/totl.rb +96 -0
- data/lib/reactive_freight/carriers/wrds.rb +218 -0
- data/lib/reactive_freight/configuration/carriers/btvp.yml +139 -0
- data/lib/reactive_freight/configuration/carriers/clni.yml +107 -0
- data/lib/reactive_freight/configuration/carriers/ctbv.yml +117 -0
- data/lib/reactive_freight/configuration/carriers/dphe.yml +124 -0
- data/lib/reactive_freight/configuration/carriers/drrq.yml +115 -0
- data/lib/reactive_freight/configuration/carriers/fcsy.yml +104 -0
- data/lib/reactive_freight/configuration/carriers/fwda.yml +117 -0
- data/lib/reactive_freight/configuration/carriers/jfj_transportation.yml +2 -0
- data/lib/reactive_freight/configuration/carriers/pens.yml +22 -0
- data/lib/reactive_freight/configuration/carriers/rdfs.yml +135 -0
- data/lib/reactive_freight/configuration/carriers/saia.yml +117 -0
- data/lib/reactive_freight/configuration/carriers/sefl.yml +115 -0
- data/lib/reactive_freight/configuration/carriers/totl.yml +107 -0
- data/lib/reactive_freight/configuration/carriers/wrds.yml +19 -0
- data/lib/reactive_freight/configuration/platforms/carrier_logistics.yml +25 -0
- data/lib/reactive_freight/configuration/platforms/liftoff.yml +12 -0
- data/lib/reactive_freight/package.rb +137 -0
- data/lib/reactive_freight/platform.rb +36 -0
- data/lib/reactive_freight/platforms.rb +4 -0
- data/lib/reactive_freight/platforms/carrier_logistics.rb +317 -0
- data/lib/reactive_freight/platforms/liftoff.rb +102 -0
- data/lib/reactive_freight/rate_estimate.rb +113 -0
- data/lib/reactive_freight/shipment_event.rb +10 -0
- data/reactive_freight.gemspec +39 -0
- data/service_type_symbols.txt +4 -0
- metadata +198 -0
@@ -0,0 +1,96 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ReactiveShipping
|
4
|
+
class TOTL < CarrierLogistics
|
5
|
+
REACTIVE_FREIGHT_CARRIER = true
|
6
|
+
|
7
|
+
cattr_reader :name, :scac
|
8
|
+
@@name = 'Total Transportation'
|
9
|
+
@@scac = 'TOTL'
|
10
|
+
|
11
|
+
def requirements
|
12
|
+
%i[username password account]
|
13
|
+
end
|
14
|
+
|
15
|
+
# Documents
|
16
|
+
|
17
|
+
# Rates
|
18
|
+
def build_calculated_accessorials(*); end
|
19
|
+
|
20
|
+
# Tracking
|
21
|
+
|
22
|
+
# protected
|
23
|
+
|
24
|
+
# Documents
|
25
|
+
|
26
|
+
# Rates
|
27
|
+
|
28
|
+
def parse_rate_response(origin, destination, packages, response)
|
29
|
+
success = true
|
30
|
+
message = ''
|
31
|
+
|
32
|
+
if !response
|
33
|
+
success = false
|
34
|
+
message = 'API Error: Unknown response'
|
35
|
+
else
|
36
|
+
response = response.parsed_response
|
37
|
+
if response['error']
|
38
|
+
success = false
|
39
|
+
message = response['error']
|
40
|
+
else
|
41
|
+
cost = response.dig('ratequote', 'quotetotal').delete(',').delete('.').to_i
|
42
|
+
transit_days = response.dig('ratequote', 'busdays').to_i
|
43
|
+
if cost
|
44
|
+
# Carrier-specific pricing structure
|
45
|
+
oversized_pallets_price = 0
|
46
|
+
packages.each do |package|
|
47
|
+
short_side, long_side = nil
|
48
|
+
if !package.length(:in).blank? && !package.width(:in).blank? && !package.height(:in).blank?
|
49
|
+
long_side = package.length(:in) > package.width(:in) ? package.length(:in) : package.width(:in)
|
50
|
+
short_side = package.length(:in) < package.width(:in) ? package.length(:in) : package.width(:in)
|
51
|
+
end
|
52
|
+
|
53
|
+
next unless short_side &&
|
54
|
+
long_side &&
|
55
|
+
package.height(:in) &&
|
56
|
+
(
|
57
|
+
short_side > 40 ||
|
58
|
+
long_side > 48 ||
|
59
|
+
package.height(:in) > 84
|
60
|
+
)
|
61
|
+
|
62
|
+
oversized_pallets_price += 1500
|
63
|
+
end
|
64
|
+
cost += oversized_pallets_price
|
65
|
+
|
66
|
+
rate_estimates = [
|
67
|
+
RateEstimate.new(
|
68
|
+
origin,
|
69
|
+
destination,
|
70
|
+
{ scac: self.class.scac.upcase, name: self.class.name },
|
71
|
+
:standard,
|
72
|
+
transit_days: transit_days,
|
73
|
+
estimate_reference: nil,
|
74
|
+
total_price: cost,
|
75
|
+
currency: 'USD',
|
76
|
+
with_excessive_length_fees: @conf.dig(:attributes, :rates, :with_excessive_length_fees)
|
77
|
+
)
|
78
|
+
]
|
79
|
+
else
|
80
|
+
success = false
|
81
|
+
message = 'API Error: Cost is emtpy'
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
RateResponse.new(
|
87
|
+
success,
|
88
|
+
message,
|
89
|
+
response.to_hash,
|
90
|
+
rates: rate_estimates,
|
91
|
+
response: response,
|
92
|
+
request: last_request
|
93
|
+
)
|
94
|
+
end
|
95
|
+
end
|
96
|
+
end
|
@@ -0,0 +1,218 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ReactiveShipping
|
4
|
+
class WRDS < ReactiveShipping::Carrier
|
5
|
+
REACTIVE_FREIGHT_CARRIER = true
|
6
|
+
|
7
|
+
cattr_reader :name, :scac
|
8
|
+
@@name = 'Western Regional Delivery Service'
|
9
|
+
@@scac = 'WRDS'
|
10
|
+
|
11
|
+
def available_services
|
12
|
+
nil
|
13
|
+
end
|
14
|
+
|
15
|
+
def requirements
|
16
|
+
%i[username password]
|
17
|
+
end
|
18
|
+
|
19
|
+
# Documents
|
20
|
+
def find_pod(tracking_number, options = {})
|
21
|
+
options = @options.merge(options)
|
22
|
+
parse_pod_response(tracking_number, options)
|
23
|
+
end
|
24
|
+
|
25
|
+
# Rates
|
26
|
+
|
27
|
+
# Tracking
|
28
|
+
def find_tracking_info(tracking_number)
|
29
|
+
parse_tracking_response(tracking_number)
|
30
|
+
end
|
31
|
+
|
32
|
+
protected
|
33
|
+
|
34
|
+
def build_url(action, *)
|
35
|
+
url = "#{@conf.dig(:api, :domain)}#{@conf.dig(:api, :endpoints, action)}"
|
36
|
+
end
|
37
|
+
|
38
|
+
def commit(action, options = {})
|
39
|
+
options = @options.merge(options)
|
40
|
+
url = request_url(action)
|
41
|
+
|
42
|
+
response = if @conf.dig(:api, :methods, action) == :post
|
43
|
+
options[:params].blank? ? HTTParty.post(url) : HTTParty.post(url, query: options[:params])
|
44
|
+
else
|
45
|
+
HTTParty.get(url)
|
46
|
+
end
|
47
|
+
|
48
|
+
response.parsed_response if response&.parsed_response
|
49
|
+
end
|
50
|
+
|
51
|
+
def request_url(action)
|
52
|
+
scheme = @conf.dig(:api, :use_ssl, action) ? 'https://' : 'http://'
|
53
|
+
"#{scheme}#{@conf.dig(:api, :domain)}#{@conf.dig(:api, :endpoints, action)}"
|
54
|
+
end
|
55
|
+
|
56
|
+
# Documents
|
57
|
+
def parse_document_response(type, tracking_number, url, options = {})
|
58
|
+
options = @options.merge(options)
|
59
|
+
path = if options[:path].blank?
|
60
|
+
File.join(Dir.tmpdir, "#{@@name} #{tracking_number} #{type.to_s.upcase}.pdf")
|
61
|
+
else
|
62
|
+
options[:path]
|
63
|
+
end
|
64
|
+
file = Tempfile.new
|
65
|
+
|
66
|
+
File.open(file.path, 'wb') do |file|
|
67
|
+
URI.parse(url).open do |input|
|
68
|
+
file.write(input.read)
|
69
|
+
end
|
70
|
+
rescue OpenURI::HTTPError
|
71
|
+
raise ReactiveShipping::ResponseError, "API Error: #{@@name}: Document not found"
|
72
|
+
end
|
73
|
+
|
74
|
+
file = Magick::ImageList.new(file.path)
|
75
|
+
file.write(path)
|
76
|
+
File.exist?(path) ? path : false
|
77
|
+
end
|
78
|
+
|
79
|
+
def parse_pod_response(tracking_number, options = {})
|
80
|
+
options = @options.merge(options)
|
81
|
+
browser = Watir::Browser.new(:chrome, headless: !@debug)
|
82
|
+
browser.goto(build_url(:pod))
|
83
|
+
|
84
|
+
browser.text_field(name: 'ctl00$cphMain$txtUserName').set(@options[:username])
|
85
|
+
browser.text_field(name: 'ctl00$cphMain$txtPassword').set(@options[:password])
|
86
|
+
browser.button(name: 'ctl00$cphMain$btnLogIn').click
|
87
|
+
|
88
|
+
browser.text_field(name: 'ctl00$cphMain$txtProNumber').set(tracking_number)
|
89
|
+
browser.button(name: 'ctl00$cphMain$btnSearchProNumber').click
|
90
|
+
browser.element(xpath: '/html/body/form/div[3]/div/div/table/tbody/tr[2]/td[1]/a').click
|
91
|
+
browser.element(xpath: '/html/body/form/div[3]/table[2]/tbody/tr[16]/td[2]/a').click
|
92
|
+
|
93
|
+
image_url = nil
|
94
|
+
browser.windows.last.use do
|
95
|
+
page_count = browser.element(xpath: '/html/body/form/div[3]/b/span').text.strip.to_i
|
96
|
+
(page_count - 1).times do
|
97
|
+
browser.element(xpath: '/html/body/form/div[3]/input[2]').click
|
98
|
+
end
|
99
|
+
image_url = browser.element(css: '#cphMain_imgImage').attribute_value('src')
|
100
|
+
end
|
101
|
+
|
102
|
+
parse_document_response(:pod, tracking_number, image_url, options)
|
103
|
+
end
|
104
|
+
|
105
|
+
# Rates
|
106
|
+
|
107
|
+
# Tracking
|
108
|
+
def parse_city_state_zip(str)
|
109
|
+
return nil if str.blank?
|
110
|
+
|
111
|
+
Location.new(
|
112
|
+
city: str.split(', ')[0].titleize,
|
113
|
+
state: str.split(', ')[1].split(' ')[0].upcase,
|
114
|
+
zip_code: str.split(', ')[1].split(' ')[1],
|
115
|
+
country: ActiveUtils::Country.find('USA')
|
116
|
+
)
|
117
|
+
end
|
118
|
+
|
119
|
+
def parse_city_state(str)
|
120
|
+
return nil if str.blank?
|
121
|
+
|
122
|
+
Location.new(
|
123
|
+
city: str.split(' ')[0].titleize,
|
124
|
+
state: str.split(' ')[1].upcase,
|
125
|
+
country: ActiveUtils::Country.find('USA')
|
126
|
+
)
|
127
|
+
end
|
128
|
+
|
129
|
+
def parse_date(date)
|
130
|
+
date ? DateTime.strptime(date, '%m/%d/%Y %l:%M:%S %p').to_s(:db) : nil
|
131
|
+
end
|
132
|
+
|
133
|
+
def parse_tracking_response(tracking_number)
|
134
|
+
browser = Watir::Browser.new(:chrome, headless: !@debug)
|
135
|
+
browser.goto build_url(:track)
|
136
|
+
|
137
|
+
browser.text_field(name: 'ctl00$cphMain$txtProNumber').set(tracking_number)
|
138
|
+
browser.button(name: 'ctl00$cphMain$btnSearchProNumber').click
|
139
|
+
browser.element(xpath: '/html/body/form/div[3]/div/div/table/tbody/tr[2]/td[1]/a').click
|
140
|
+
html = browser.table(id: 'cphMain_grvLogNotes').inner_html
|
141
|
+
html = Nokogiri::HTML(html)
|
142
|
+
|
143
|
+
shipper_address = parse_city_state_zip(
|
144
|
+
browser.element(
|
145
|
+
xpath: '/html/body/form/div[3]/table[2]/tbody/tr[14]/td[1]/span'
|
146
|
+
).text
|
147
|
+
)
|
148
|
+
|
149
|
+
receiver_address = parse_city_state_zip(
|
150
|
+
browser.element(
|
151
|
+
xpath: '/html/body/form/div[3]/table[2]/tbody/tr[14]/td[2]/span'
|
152
|
+
).text
|
153
|
+
)
|
154
|
+
|
155
|
+
ship_time = browser.element(
|
156
|
+
xpath: '/html/body/form/div[3]/table[2]/tbody/tr[7]/td[2]/span'
|
157
|
+
).text
|
158
|
+
ship_time = ship_time ? Date.strptime(ship_time, '%m/%d/%Y').to_s(:db) : nil
|
159
|
+
|
160
|
+
shipment_events = []
|
161
|
+
html.css('tr').each do |tr|
|
162
|
+
next if tr.text.include?('DateNotes')
|
163
|
+
|
164
|
+
datetime_without_time_zone = tr.css('td')[0].text
|
165
|
+
event = tr.css('td')[1].text
|
166
|
+
|
167
|
+
event_key = nil
|
168
|
+
@conf.dig(:events, :types).each do |key, val|
|
169
|
+
if event.downcase.include?(val) && !event.downcase.include?('estimated')
|
170
|
+
event_key = key
|
171
|
+
break
|
172
|
+
end
|
173
|
+
end
|
174
|
+
next if event_key.blank?
|
175
|
+
|
176
|
+
location = event.downcase.split(@conf.dig(:events, :types, event_key)).last
|
177
|
+
location = location.downcase.sub(event_key.to_s, '')
|
178
|
+
location = location.gsub(',', '')
|
179
|
+
location = location.downcase.include?('carrier') ? nil : parse_city_state(location)
|
180
|
+
|
181
|
+
event = event_key
|
182
|
+
datetime_without_time_zone = parse_date(datetime_without_time_zone)
|
183
|
+
|
184
|
+
# status and type_code set automatically by ActiveFreight based on event
|
185
|
+
shipment_events << ShipmentEvent.new(event, datetime_without_time_zone, location)
|
186
|
+
end
|
187
|
+
|
188
|
+
scheduled_delivery_date = nil
|
189
|
+
status = shipment_events.last.status
|
190
|
+
|
191
|
+
actual_delivery_date = browser.element(xpath: '/html/body/form/div[3]/table[2]/tbody/tr[9]/td[2]/span').text
|
192
|
+
actual_delivery_date = actual_delivery_date ? Date.strptime(actual_delivery_date, '%m/%d/%Y').to_s(:db) : nil
|
193
|
+
|
194
|
+
shipment_events = shipment_events.sort_by(&:time)
|
195
|
+
|
196
|
+
TrackingResponse.new(
|
197
|
+
true,
|
198
|
+
status,
|
199
|
+
{ html: html.to_s },
|
200
|
+
carrier: "#{@@scac}, #{@@name}",
|
201
|
+
html: html,
|
202
|
+
response: html.to_s,
|
203
|
+
status: status,
|
204
|
+
type_code: status,
|
205
|
+
ship_time: ship_time,
|
206
|
+
scheduled_delivery_date: scheduled_delivery_date,
|
207
|
+
actual_delivery_date: actual_delivery_date,
|
208
|
+
delivery_signature: nil,
|
209
|
+
shipment_events: shipment_events,
|
210
|
+
shipper_address: shipper_address,
|
211
|
+
origin: shipper_address,
|
212
|
+
destination: receiver_address,
|
213
|
+
tracking_number: tracking_number,
|
214
|
+
request: last_request
|
215
|
+
)
|
216
|
+
end
|
217
|
+
end
|
218
|
+
end
|
@@ -0,0 +1,139 @@
|
|
1
|
+
:accessorials:
|
2
|
+
:mappable:
|
3
|
+
:appointment_delivery: NOTFEE
|
4
|
+
:inside_delivery: INSDEL
|
5
|
+
:liftgate_delivery: LIFDEL
|
6
|
+
:liftgate_pickup: LIFPIC
|
7
|
+
:residential_delivery: RESDEL
|
8
|
+
:residential_pickup: RESPIC
|
9
|
+
# May be inaccurate
|
10
|
+
:unquotable:
|
11
|
+
- :athletic_facility_delivery
|
12
|
+
- :athletic_facility_pickup
|
13
|
+
- :cemetery_delivery
|
14
|
+
- :cemetery_pickup
|
15
|
+
- :church_delivery
|
16
|
+
- :church_pickup
|
17
|
+
- :construction_delivery
|
18
|
+
- :construction_pickup
|
19
|
+
- :dock_dropoff
|
20
|
+
- :dock_pickup
|
21
|
+
- :farm_delivery
|
22
|
+
- :farm_pickup
|
23
|
+
- :fitness_center_delivery
|
24
|
+
- :fitness_center_pickup
|
25
|
+
- :flatbed_delivery
|
26
|
+
- :flatbed_pickup
|
27
|
+
- :golf_course_delivery
|
28
|
+
- :golf_course_pickup
|
29
|
+
- :government_delivery
|
30
|
+
- :government_pickup
|
31
|
+
- :grocery_store_delivery
|
32
|
+
- :grocery_store_pickup
|
33
|
+
- :holiday_delivery
|
34
|
+
- :holiday_pickup
|
35
|
+
- :hospital_delivery
|
36
|
+
- :hospital_pickup
|
37
|
+
- :hotel_delivery
|
38
|
+
- :hotel_pickup
|
39
|
+
- :inspection_site_delivery
|
40
|
+
- :inspection_site_pickup
|
41
|
+
- :jobsite_delivery
|
42
|
+
- :jobsite_pickup
|
43
|
+
- :mall_delivery
|
44
|
+
- :mall_pickup
|
45
|
+
- :military_site_delivery
|
46
|
+
- :military_site_pickup
|
47
|
+
- :mine_site_delivery
|
48
|
+
- :mine_site_pickup
|
49
|
+
- :motel_delivery
|
50
|
+
- :motel_pickup
|
51
|
+
- :nursing_home_delivery
|
52
|
+
- :nursing_home_pickup
|
53
|
+
- :park_delivery
|
54
|
+
- :park_pickup
|
55
|
+
- :prison_delivery
|
56
|
+
- :prison_pickup
|
57
|
+
- :ranch_delivery
|
58
|
+
- :ranch_pickup
|
59
|
+
- :reservation_delivery
|
60
|
+
- :reservation_pickup
|
61
|
+
- :resort_delivery
|
62
|
+
- :resort_pickup
|
63
|
+
- :restaurant_delivery
|
64
|
+
- :restaurant_pickup
|
65
|
+
- :school_delivery
|
66
|
+
- :school_pickup
|
67
|
+
- :steel_mill_delivery
|
68
|
+
- :steel_mill_pickup
|
69
|
+
- :storage_facility_delivery
|
70
|
+
- :storage_facility_pickup
|
71
|
+
- :university_delivery
|
72
|
+
- :university_pickup
|
73
|
+
- :utility_site_delivery
|
74
|
+
- :utility_site_pickup
|
75
|
+
# May be inaccurate
|
76
|
+
:unserviceable:
|
77
|
+
- :afterhours_delivery
|
78
|
+
- :airport_delivery
|
79
|
+
- :airport_pickup
|
80
|
+
- :amusement_park_delivery
|
81
|
+
- :amusement_park_pickup
|
82
|
+
- :appointment_pickup
|
83
|
+
- :brewery_delivery
|
84
|
+
- :brewery_pickup
|
85
|
+
- :convention_delivery
|
86
|
+
- :convention_pickup
|
87
|
+
- :customs_brokerage
|
88
|
+
- :driver_assist
|
89
|
+
- :early_morning_delivery
|
90
|
+
- :fair_delivery
|
91
|
+
- :fair_pickup
|
92
|
+
- :grocery_warehouse_delivery
|
93
|
+
- :grocery_warehouse_pickup
|
94
|
+
- :guaranteed_delivery
|
95
|
+
- :guaranteed_delivery_am
|
96
|
+
- :guaranteed_delivery_pm
|
97
|
+
- :inside_pickup
|
98
|
+
- :marina_delivery
|
99
|
+
- :marina_pickup
|
100
|
+
- :saturday_delivery
|
101
|
+
- :saturday_pickup
|
102
|
+
- :sunday_delivery
|
103
|
+
- :sunday_pickup
|
104
|
+
- :winery_delivery
|
105
|
+
- :winery_pickup
|
106
|
+
:api:
|
107
|
+
:actions:
|
108
|
+
:rates: :getquote
|
109
|
+
:track: :tracktrace
|
110
|
+
:domain: tgif1.bestovernite.com
|
111
|
+
:endpoints:
|
112
|
+
:bol: /xr0001.pgm
|
113
|
+
:pod: /xr0001.pgm
|
114
|
+
:rates: /web/services/TQUOTEAPIService/TQUOTEAPI?wsdl
|
115
|
+
:track: /web/services/TTRACKAPIService/TTRACKAPI?wsdl
|
116
|
+
:ports:
|
117
|
+
:bol: 8090
|
118
|
+
:pod: 8090
|
119
|
+
:rates: 10032
|
120
|
+
:track: 10032
|
121
|
+
:use_ssl:
|
122
|
+
:rates: false
|
123
|
+
:track: false
|
124
|
+
:attributes:
|
125
|
+
:rates:
|
126
|
+
:with_excessive_length_fees: false
|
127
|
+
:documents:
|
128
|
+
:types:
|
129
|
+
:bol: Bill of Lading
|
130
|
+
:pod: Proof of Delivery
|
131
|
+
:events:
|
132
|
+
:types:
|
133
|
+
:arrived_at_terminal: arrived
|
134
|
+
:delivered: delivered
|
135
|
+
:departed: departed
|
136
|
+
:out_for_delivery: out for delvry
|
137
|
+
:services:
|
138
|
+
:types:
|
139
|
+
- :standard
|