reactive_freight 0.0.1

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.
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