reactive_freight 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (49) hide show
  1. checksums.yaml +7 -0
  2. data/Gemfile +5 -0
  3. data/MIT-LICENSE +20 -0
  4. data/README.md +166 -0
  5. data/Rakefile +8 -0
  6. data/accessorial_symbols.txt +95 -0
  7. data/lib/reactive_freight.rb +21 -0
  8. data/lib/reactive_freight/carrier.rb +62 -0
  9. data/lib/reactive_freight/carriers.rb +18 -0
  10. data/lib/reactive_freight/carriers/btvp.rb +384 -0
  11. data/lib/reactive_freight/carriers/clni.rb +59 -0
  12. data/lib/reactive_freight/carriers/ctbv.rb +35 -0
  13. data/lib/reactive_freight/carriers/dphe.rb +296 -0
  14. data/lib/reactive_freight/carriers/drrq.rb +303 -0
  15. data/lib/reactive_freight/carriers/fcsy.rb +24 -0
  16. data/lib/reactive_freight/carriers/fwda.rb +243 -0
  17. data/lib/reactive_freight/carriers/jfj_transportation.rb +11 -0
  18. data/lib/reactive_freight/carriers/pens.rb +135 -0
  19. data/lib/reactive_freight/carriers/rdfs.rb +320 -0
  20. data/lib/reactive_freight/carriers/saia.rb +336 -0
  21. data/lib/reactive_freight/carriers/sefl.rb +234 -0
  22. data/lib/reactive_freight/carriers/totl.rb +96 -0
  23. data/lib/reactive_freight/carriers/wrds.rb +218 -0
  24. data/lib/reactive_freight/configuration/carriers/btvp.yml +139 -0
  25. data/lib/reactive_freight/configuration/carriers/clni.yml +107 -0
  26. data/lib/reactive_freight/configuration/carriers/ctbv.yml +117 -0
  27. data/lib/reactive_freight/configuration/carriers/dphe.yml +124 -0
  28. data/lib/reactive_freight/configuration/carriers/drrq.yml +115 -0
  29. data/lib/reactive_freight/configuration/carriers/fcsy.yml +104 -0
  30. data/lib/reactive_freight/configuration/carriers/fwda.yml +117 -0
  31. data/lib/reactive_freight/configuration/carriers/jfj_transportation.yml +2 -0
  32. data/lib/reactive_freight/configuration/carriers/pens.yml +22 -0
  33. data/lib/reactive_freight/configuration/carriers/rdfs.yml +135 -0
  34. data/lib/reactive_freight/configuration/carriers/saia.yml +117 -0
  35. data/lib/reactive_freight/configuration/carriers/sefl.yml +115 -0
  36. data/lib/reactive_freight/configuration/carriers/totl.yml +107 -0
  37. data/lib/reactive_freight/configuration/carriers/wrds.yml +19 -0
  38. data/lib/reactive_freight/configuration/platforms/carrier_logistics.yml +25 -0
  39. data/lib/reactive_freight/configuration/platforms/liftoff.yml +12 -0
  40. data/lib/reactive_freight/package.rb +137 -0
  41. data/lib/reactive_freight/platform.rb +36 -0
  42. data/lib/reactive_freight/platforms.rb +4 -0
  43. data/lib/reactive_freight/platforms/carrier_logistics.rb +317 -0
  44. data/lib/reactive_freight/platforms/liftoff.rb +102 -0
  45. data/lib/reactive_freight/rate_estimate.rb +113 -0
  46. data/lib/reactive_freight/shipment_event.rb +10 -0
  47. data/reactive_freight.gemspec +39 -0
  48. data/service_type_symbols.txt +4 -0
  49. 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