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.
- 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
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 17738be42395599ca8147e6dbcd058e3829f7d72978471ff9c8bda4bbac09716
|
4
|
+
data.tar.gz: b2d39f6d615b2cd4cc298a0ab0c9897d1f7ebae626fdf2c7760e730aa92faae8
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 52059a010d29aecae940475ad22a1eccb7d4d1abaf14e74b62d879c61a97ef1aa233dc2bd2afd3605b8ecad8540222abe7453e418f340b3d3106d8902e9dcaa9
|
7
|
+
data.tar.gz: 3d0acc30c3db90aff7d273ba3e4931324acff2c8c44cf59e25029a0e3367e449430ddb5a54795539e8e272b53f6c5832bb595a7434e715a6b78e7ef556982230
|
data/Gemfile
ADDED
data/MIT-LICENSE
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright (c) 2020 Brody Hoskins
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
4
|
+
a copy of this software and associated documentation files (the
|
5
|
+
"Software"), to deal in the Software without restriction, including
|
6
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
7
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
8
|
+
permit persons to whom the Software is furnished to do so, subject to
|
9
|
+
the following conditions:
|
10
|
+
|
11
|
+
The above copyright notice and this permission notice shall be
|
12
|
+
included in all copies or substantial portions of the Software.
|
13
|
+
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
15
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
16
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
17
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
18
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
19
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
20
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,166 @@
|
|
1
|
+
[](https://github.com/rubocop-hq/rubocop)
|
2
|
+

|
3
|
+
|
4
|
+
# ReactiveFreight
|
5
|
+
|
6
|
+
ReactiveFreight extends [ReactiveShipping](https://github.com/realsubpop/reactive_shipping) to support LTL carriers.
|
7
|
+
|
8
|
+
Features specific to ReactiveFreight:
|
9
|
+
|
10
|
+
**Important:** The following features require carriers to be defined as a ReactiveFreight carriers specifically; this means that carriers included with ReactiveShipping function the same as before (and do not inherit the features).
|
11
|
+
|
12
|
+
- Abstracted accessorials
|
13
|
+
- Abstracted tracking events
|
14
|
+
- Cubic feet and density calculations
|
15
|
+
- Freight class calculations (and manual overriding)
|
16
|
+
- Download scanned documents including bill of lading and/or proof of delivery where supported
|
17
|
+
|
18
|
+
## Supported Freight Carriers & Platforms
|
19
|
+
|
20
|
+
*This list varies day to day as this the project is a work in progress*
|
21
|
+
|
22
|
+
**In addition** to the carriers supported by [ReactiveShipping](https://github.com/realsubpop/reactive_shipping), ReactiveFreight supports the following carriers and platforms.
|
23
|
+
|
24
|
+
Carriers differ from platforms in that they have unique web services whereas platforms host several carriers' web services on a single service (platform). Carriers however may extend platforms and override them for carrier-specific functionality.
|
25
|
+
|
26
|
+
### Carriers
|
27
|
+
|
28
|
+
|Carrier |BOL|POD|Rates|Tracking|
|
29
|
+
|-----------------------------------|---|---|-----|--------|
|
30
|
+
|Best Overnite Express |✓ |✓ |✓ |✓ |
|
31
|
+
|Clear Lane Freight Systems |✓ |✓ |✓ |✓ |
|
32
|
+
|The Custom Companies | | |✓ |✓ |
|
33
|
+
|Dependable Highway Express | | |✓ |✓ |
|
34
|
+
|Forward Air | |✓ |✓ |✓ |
|
35
|
+
|Frontline Freight |✓ |✓ |✓ |✓ |
|
36
|
+
|Peninsula Truck Lines | | |✓ | |
|
37
|
+
|Roadrunner Transportation Services |✓ |✓ |✓ |✓ |
|
38
|
+
|Saia | | |✓ |✓ |
|
39
|
+
|Southeastern Freight Lines | | |✓ | |
|
40
|
+
|Tforce Worldwide | |✓ |✓ | |
|
41
|
+
|Total Transportation & Distribution|✓ |✓ |✓ |✓ |
|
42
|
+
|Western Regional Delivery Service | |✓ | |✓ |
|
43
|
+
|
44
|
+
### Platforms
|
45
|
+
|
46
|
+
* [Carrier Logistics](https://carrierlogistics.com)
|
47
|
+
|
48
|
+
## Versions
|
49
|
+
|
50
|
+
[See releases](https://github.com/brodyhoskins/reactive_freight/releases)
|
51
|
+
|
52
|
+
## Installation
|
53
|
+
|
54
|
+
Using bundler, add to the `Gemfile`:
|
55
|
+
|
56
|
+
```ruby
|
57
|
+
gem 'reactive_freight'
|
58
|
+
```
|
59
|
+
|
60
|
+
Or stand alone:
|
61
|
+
|
62
|
+
```
|
63
|
+
$ gem install reactive_freight
|
64
|
+
```
|
65
|
+
|
66
|
+
## Sample Usage
|
67
|
+
|
68
|
+
Start off by initializing the carrier:
|
69
|
+
|
70
|
+
```ruby
|
71
|
+
require 'reactive_freight'
|
72
|
+
carrier = ReactiveShipping::BTVP.new(account: 'account_number',
|
73
|
+
username: 'username',
|
74
|
+
password: 'password')
|
75
|
+
```
|
76
|
+
|
77
|
+
### Documents
|
78
|
+
|
79
|
+
```ruby
|
80
|
+
carrier.find_bol(tracking_number)
|
81
|
+
carrier.find_pod(tracking_number, path: 'POD.pdf') # path is optional
|
82
|
+
```
|
83
|
+
|
84
|
+
### Tracking
|
85
|
+
|
86
|
+
**Important:** When a ReactiveFreight carrier is loaded `ReactiveShipping::ShipmentEvent` objects' `name` and `status` will return symbols rather than text — it is up to higher-level libraries to provide translations.
|
87
|
+
|
88
|
+
Carriers included with ReactiveShipping (typically non-freight) will retain the original behavior for compatibility.
|
89
|
+
|
90
|
+
```ruby
|
91
|
+
tracking = carrier.find_tracking_info(tracking_number)
|
92
|
+
|
93
|
+
tracking.delivered?
|
94
|
+
tracking.status
|
95
|
+
|
96
|
+
tracking_info.shipment_events.each do |event|
|
97
|
+
puts "#{event.name} at #{event.location.city}, #{event.location.state} on #{event.time}. #{event.message}"
|
98
|
+
end
|
99
|
+
```
|
100
|
+
|
101
|
+
### Quoting
|
102
|
+
|
103
|
+
**Note:** Dimensions from ReactiveShipping were passed as an array in `height x width x length` order. While this is still supported, explicitly setting dimensions in a hash (as demonstrated below) is highly recommended to reduce confusion.
|
104
|
+
|
105
|
+
```ruby
|
106
|
+
packages = [
|
107
|
+
ReactiveShipping::Package.new(371 * 16, # 371 lbs
|
108
|
+
{
|
109
|
+
length: 40, # inches
|
110
|
+
width: 48,
|
111
|
+
height: 47
|
112
|
+
},
|
113
|
+
units: :imperial),
|
114
|
+
ReactiveShipping::Package.new(371 * 16, # 371 lbs
|
115
|
+
{
|
116
|
+
length: 40, # inches
|
117
|
+
width: 48,
|
118
|
+
height: 47
|
119
|
+
},
|
120
|
+
freight_class: 125, # override calculated freight class
|
121
|
+
units: :imperial)
|
122
|
+
]
|
123
|
+
|
124
|
+
origin = ReactiveShipping::Location.new(country: 'US',
|
125
|
+
state: 'CA',
|
126
|
+
city: 'Los Angeles',
|
127
|
+
zip: '90001')
|
128
|
+
|
129
|
+
destination = ReactiveShipping::Location.new(country: 'US',
|
130
|
+
state: 'IL',
|
131
|
+
city: 'Chicago',
|
132
|
+
zip: '60007')
|
133
|
+
|
134
|
+
accessorials = %i[
|
135
|
+
appointment_delivery
|
136
|
+
liftgate_delivery
|
137
|
+
residential_delivery
|
138
|
+
]
|
139
|
+
|
140
|
+
response = carrier.find_rates(origin, destination, packages, accessorials: accessorials)
|
141
|
+
rates = response.rates
|
142
|
+
rates = response.rates.sort_by(&:price).collect { |rate| [rate.service_name, rate.price] }
|
143
|
+
```
|
144
|
+
|
145
|
+
**Important:** A ReactiveFreight `RateEstimate` returns a `Hash` rather than a `String` with the carrier's name; stock ReactiveShipping `RateEstimates` return the latter:
|
146
|
+
|
147
|
+
```ruby
|
148
|
+
rate = rates.first
|
149
|
+
rate.carrier
|
150
|
+
|
151
|
+
=> "Best Overnite Express" # ReactiveShipping
|
152
|
+
=> {:scac=>"BTVP", :name=>"Best Overnite Express"} # ReactiveFreight
|
153
|
+
|
154
|
+
# To find the relevant information, check the hash
|
155
|
+
rate.carrier.dig(:name)
|
156
|
+
=> "Best Overnite Express"
|
157
|
+
rate.carrier.dig(:scac)
|
158
|
+
=> "BTVP"
|
159
|
+
|
160
|
+
# Maintain compatibility with ReactiveShipping
|
161
|
+
rate.carrier.is_a?(Hash) ? rate.carrier.dig(:name) : rate.carrier
|
162
|
+
=> "Best Overnite Express"
|
163
|
+
|
164
|
+
rate.carrier.is_a?(Hash) ? rate.carrier.dig(:scac) : nil
|
165
|
+
=> "BTVP"
|
166
|
+
```
|
data/Rakefile
ADDED
@@ -0,0 +1,95 @@
|
|
1
|
+
:afterhours_delivery
|
2
|
+
:afterhours_pickup
|
3
|
+
:airport_delivery
|
4
|
+
:airport_pickup
|
5
|
+
:amusement_park_delivery
|
6
|
+
:amusement_park_pickup
|
7
|
+
:appointment_delivery
|
8
|
+
:appointment_pickup
|
9
|
+
:athletic_facility_delivery
|
10
|
+
:athletic_facility_pickup
|
11
|
+
:brewery_delivery
|
12
|
+
:brewery_pickup
|
13
|
+
:cemetery_delivery
|
14
|
+
:cemetery_pickup
|
15
|
+
:church_delivery
|
16
|
+
:church_pickup
|
17
|
+
:construction_delivery
|
18
|
+
:construction_pickup
|
19
|
+
:convention_delivery
|
20
|
+
:convention_pickup
|
21
|
+
:customs_brokerage
|
22
|
+
:dock_dropoff
|
23
|
+
:dock_pickup
|
24
|
+
:driver_assist
|
25
|
+
:early_morning_delivery
|
26
|
+
:early_morning_pickup
|
27
|
+
:fair_delivery
|
28
|
+
:fair_pickup
|
29
|
+
:farm_delivery
|
30
|
+
:farm_pickup
|
31
|
+
:fitness_center_delivery
|
32
|
+
:fitness_center_pickup
|
33
|
+
:flatbed_delivery
|
34
|
+
:flatbed_pickup
|
35
|
+
:golf_course_delivery
|
36
|
+
:golf_course_pickup
|
37
|
+
:government_delivery
|
38
|
+
:government_pickup
|
39
|
+
:grocery_store_delivery
|
40
|
+
:grocery_store_pickup
|
41
|
+
:grocery_warehouse_delivery
|
42
|
+
:grocery_warehouse_pickup
|
43
|
+
:guaranteed_delivery
|
44
|
+
:guaranteed_delivery_am
|
45
|
+
:guaranteed_delivery_pm
|
46
|
+
:hospital_delivery
|
47
|
+
:hospital_pickup
|
48
|
+
:hotel_delivery
|
49
|
+
:hotel_pickup
|
50
|
+
:inside_delivery
|
51
|
+
:inside_pickup
|
52
|
+
:inspection_site_delivery
|
53
|
+
:inspection_site_pickup
|
54
|
+
:jobsite_delivery
|
55
|
+
:jobsite_pickup
|
56
|
+
:liftgate_delivery
|
57
|
+
:liftgate_pickup
|
58
|
+
:mall_delivery
|
59
|
+
:mall_pickup
|
60
|
+
:marina_delivery
|
61
|
+
:marina_pickup
|
62
|
+
:military_site_delivery
|
63
|
+
:military_site_pickup
|
64
|
+
:mine_site_delivery
|
65
|
+
:mine_site_pickup
|
66
|
+
:motel_delivery
|
67
|
+
:motel_pickup
|
68
|
+
:nursing_home_delivery
|
69
|
+
:nursing_home_pickup
|
70
|
+
:park_delivery
|
71
|
+
:park_pickup
|
72
|
+
:prison_delivery
|
73
|
+
:prison_pickup
|
74
|
+
:ranch_delivery
|
75
|
+
:ranch_pickup
|
76
|
+
:reservation_delivery
|
77
|
+
:reservation_pickup
|
78
|
+
:residential_delivery
|
79
|
+
:residential_pickup
|
80
|
+
:resort_delivery
|
81
|
+
:resort_pickup
|
82
|
+
:restaurant_delivery
|
83
|
+
:restaurant_pickup
|
84
|
+
:school_delivery
|
85
|
+
:school_pickup
|
86
|
+
:steel_mill_delivery
|
87
|
+
:steel_mill_pickup
|
88
|
+
:storage_facility_delivery
|
89
|
+
:storage_facility_pickup
|
90
|
+
:university_delivery
|
91
|
+
:university_pickup
|
92
|
+
:utility_site_delivery
|
93
|
+
:utility_site_pickup
|
94
|
+
:winery_delivery
|
95
|
+
:winery_pickup
|
@@ -0,0 +1,21 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'httparty'
|
4
|
+
require 'nokogiri'
|
5
|
+
require 'open-uri'
|
6
|
+
require 'rmagick'
|
7
|
+
require 'savon'
|
8
|
+
require 'watir'
|
9
|
+
require 'webdrivers/chromedriver'
|
10
|
+
require 'yaml'
|
11
|
+
|
12
|
+
require 'reactive_shipping'
|
13
|
+
require 'reactive_freight/package'
|
14
|
+
require 'reactive_freight/rate_estimate'
|
15
|
+
require 'reactive_freight/shipment_event'
|
16
|
+
|
17
|
+
require 'reactive_freight/carrier'
|
18
|
+
require 'reactive_freight/platform'
|
19
|
+
|
20
|
+
require 'reactive_freight/carriers'
|
21
|
+
require 'reactive_freight/platforms'
|
@@ -0,0 +1,62 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ReactiveShipping
|
4
|
+
class Carrier
|
5
|
+
attr_accessor :conf, :rates_with_excessive_length_fees
|
6
|
+
|
7
|
+
def initialize(options = {})
|
8
|
+
requirements.each { |key| requires!(options, key) }
|
9
|
+
@conf = nil
|
10
|
+
@debug = options[:debug].blank? ? false : true
|
11
|
+
@options = options
|
12
|
+
@last_request = nil
|
13
|
+
@test_mode = @options[:test]
|
14
|
+
|
15
|
+
return unless self.class::REACTIVE_FREIGHT_CARRIER
|
16
|
+
|
17
|
+
conf_path = File.join(__dir__, 'configuration', 'carriers', "#{self.class.to_s.split('::')[1].underscore}.yml")
|
18
|
+
@conf = YAML.safe_load(File.read(conf_path), permitted_classes: [Symbol])
|
19
|
+
|
20
|
+
@rates_with_excessive_length_fees = @conf.dig(:attributes, :rates, :with_excessive_length_fees)
|
21
|
+
end
|
22
|
+
|
23
|
+
def maximum_weight
|
24
|
+
Measured::Weight.new(10_000, :pounds)
|
25
|
+
end
|
26
|
+
|
27
|
+
def serviceable_accessorials?(accessorials)
|
28
|
+
return true if accessorials.blank?
|
29
|
+
|
30
|
+
if !self.class::REACTIVE_FREIGHT_CARRIER ||
|
31
|
+
!@conf.dig(:accessorials, :mappable) ||
|
32
|
+
!@conf.dig(:accessorials, :unquotable) ||
|
33
|
+
!@conf.dig(:accessorials, :unserviceable)
|
34
|
+
raise NotImplementedError, "#{self.class.name}: #serviceable_accessorials? not supported"
|
35
|
+
end
|
36
|
+
|
37
|
+
serviceable_accessorials = @conf.dig(:accessorials, :mappable).keys + @conf.dig(:accessorials, :unquotable)
|
38
|
+
serviceable_count = (serviceable_accessorials & accessorials).size
|
39
|
+
|
40
|
+
unserviceable_accessorials = @conf.dig(:accessorials, :unserviceable)
|
41
|
+
unserviceable_count = (unserviceable_accessorials & accessorials).size
|
42
|
+
|
43
|
+
if serviceable_count != accessorials.size || !unserviceable_count.zero?
|
44
|
+
raise ArgumentError, "#{self.class.name}: Some accessorials unserviceable"
|
45
|
+
end
|
46
|
+
|
47
|
+
true
|
48
|
+
end
|
49
|
+
|
50
|
+
def find_bol(*)
|
51
|
+
raise NotImplementedError, "#{self.class.name}: #find_bol not supported"
|
52
|
+
end
|
53
|
+
|
54
|
+
def find_estimate(*)
|
55
|
+
raise NotImplementedError, "#{self.class.name}: #find_estimate not supported"
|
56
|
+
end
|
57
|
+
|
58
|
+
def find_pod(*)
|
59
|
+
raise NotImplementedError, "#{self.class.name}: #find_pod not supported"
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
ReactiveShipping::Carriers.register :BTVP, 'reactive_freight/carriers/btvp'
|
4
|
+
ReactiveShipping::Carriers.register :DPHE, 'reactive_freight/carriers/dphe'
|
5
|
+
ReactiveShipping::Carriers.register :DRRQ, 'reactive_freight/carriers/drrq'
|
6
|
+
ReactiveShipping::Carriers.register :FWDA, 'reactive_freight/carriers/fwda'
|
7
|
+
ReactiveShipping::Carriers.register :PENS, 'reactive_freight/carriers/pens'
|
8
|
+
ReactiveShipping::Carriers.register :RDFS, 'reactive_freight/carriers/rdfs'
|
9
|
+
ReactiveShipping::Carriers.register :SAIA, 'reactive_freight/carriers/saia'
|
10
|
+
ReactiveShipping::Carriers.register :SEFL, 'reactive_freight/carriers/sefl'
|
11
|
+
ReactiveShipping::Carriers.register :WRDS, 'reactive_freight/carriers/wrds'
|
12
|
+
|
13
|
+
# Carriers based on platforms
|
14
|
+
ReactiveShipping::Carriers.register :CLNI, 'reactive_freight/carriers/clni'
|
15
|
+
ReactiveShipping::Carriers.register :CTBV, 'reactive_freight/carriers/ctbv'
|
16
|
+
ReactiveShipping::Carriers.register :JFJTransportation, 'reactive_freight/carriers/jfj_transportation'
|
17
|
+
ReactiveShipping::Carriers.register :FCSY, 'reactive_freight/carriers/fcsy'
|
18
|
+
ReactiveShipping::Carriers.register :TOTL, 'reactive_freight/carriers/totl'
|
@@ -0,0 +1,384 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ReactiveShipping
|
4
|
+
class BTVP < ReactiveShipping::Carrier
|
5
|
+
REACTIVE_FREIGHT_CARRIER = true
|
6
|
+
|
7
|
+
cattr_reader :name, :scac
|
8
|
+
@@name = 'Best Overnite Express'
|
9
|
+
@@scac = 'BTVP'
|
10
|
+
|
11
|
+
def requirements
|
12
|
+
%i[username password]
|
13
|
+
end
|
14
|
+
|
15
|
+
# Documents
|
16
|
+
def find_bol(tracking_number, options = {})
|
17
|
+
options = @options.merge(options)
|
18
|
+
parse_document_response(:bol, tracking_number, options)
|
19
|
+
end
|
20
|
+
|
21
|
+
def find_pod(tracking_number, options = {})
|
22
|
+
options = @options.merge(options)
|
23
|
+
parse_document_response(:pod, tracking_number, options)
|
24
|
+
end
|
25
|
+
|
26
|
+
# Rates
|
27
|
+
def find_rates(origin, destination, packages, options = {})
|
28
|
+
options = @options.merge(options)
|
29
|
+
origin = Location.from(origin)
|
30
|
+
destination = Location.from(destination)
|
31
|
+
packages = Array(packages)
|
32
|
+
|
33
|
+
request = build_rate_request(origin, destination, packages, options)
|
34
|
+
parse_rate_response(origin, destination, packages, commit(:rates, request))
|
35
|
+
end
|
36
|
+
|
37
|
+
# Tracking
|
38
|
+
def find_tracking_info(tracking_number, options = {})
|
39
|
+
options = @options.merge(options)
|
40
|
+
request = build_tracking_request(tracking_number)
|
41
|
+
parse_tracking_response(commit(:track, request))
|
42
|
+
end
|
43
|
+
|
44
|
+
protected
|
45
|
+
|
46
|
+
def build_soap_header
|
47
|
+
{
|
48
|
+
username: @options[:username],
|
49
|
+
password: @options[:password]
|
50
|
+
}
|
51
|
+
end
|
52
|
+
|
53
|
+
def commit(action, request)
|
54
|
+
Savon.client(
|
55
|
+
wsdl: build_url(action),
|
56
|
+
convert_request_keys_to: :upcase,
|
57
|
+
env_namespace: :soapenv
|
58
|
+
).call(
|
59
|
+
@conf.dig(:api, :actions, action),
|
60
|
+
headers: { 'SOAPAction' => '""' },
|
61
|
+
soap_action: false,
|
62
|
+
message: request
|
63
|
+
).body
|
64
|
+
end
|
65
|
+
|
66
|
+
def parse_date(date)
|
67
|
+
date ? Date.strptime(date, '%m/%d/%Y').to_s(:db) : nil
|
68
|
+
end
|
69
|
+
|
70
|
+
def parse_datetime(datetime)
|
71
|
+
return nil unless datetime
|
72
|
+
|
73
|
+
if datetime.include?('-')
|
74
|
+
DateTime.strptime(datetime, '%Y-%m-%d %H:%M').to_s(:db)
|
75
|
+
else
|
76
|
+
DateTime.strptime(datetime, '%m/%d/%Y %H:%M').to_s(:db)
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
def build_url(action)
|
81
|
+
scheme = @conf.dig(:api, :use_ssl, action) ? 'https://' : 'http://'
|
82
|
+
"#{scheme}#{@conf.dig(:api, :domain)}:#{@conf.dig(:api, :ports, action)}#{@conf.dig(:api, :endpoints, action)}"
|
83
|
+
end
|
84
|
+
|
85
|
+
def strip_date(str)
|
86
|
+
str ? str.split(/[A|P]M /)[1] : nil
|
87
|
+
end
|
88
|
+
|
89
|
+
# Documents
|
90
|
+
def download_document(type, tracking_number, url, options = {})
|
91
|
+
options = @options.merge(options)
|
92
|
+
path = options[:path].blank? ? File.join(Dir.tmpdir, "#{@@name} #{tracking_number} #{type.to_s.upcase}.pdf") : options[:path]
|
93
|
+
file = File.new(path, 'w')
|
94
|
+
|
95
|
+
File.open(file.path, 'wb') do |file|
|
96
|
+
URI.parse(url).open do |input|
|
97
|
+
file.write(input.read)
|
98
|
+
end
|
99
|
+
rescue OpenURI::HTTPError
|
100
|
+
raise ReactiveShipping::ResponseError, "API Error: #{@@name}: Document not found"
|
101
|
+
end
|
102
|
+
|
103
|
+
File.exist?(path) ? path : false
|
104
|
+
end
|
105
|
+
|
106
|
+
def parse_document_response(type, tracking_number, options = {})
|
107
|
+
options = @options.merge(options)
|
108
|
+
browser = Watir::Browser.new(:chrome, headless: !@debug)
|
109
|
+
browser.goto(build_url(:pod))
|
110
|
+
|
111
|
+
browser.text_field(name: 'userid').set(@options[:username])
|
112
|
+
browser.text_field(name: 'password').set(@options[:password])
|
113
|
+
browser.button(name: 'btnLogin').click
|
114
|
+
|
115
|
+
browser.element(xpath: '/html/body/div[1]/div[2]/div[2]/div[1]/div[2]/img').click
|
116
|
+
browser.element(xpath: '/html/body/div[1]/div[2]/div[2]/div[1]/div[3]/ul[2]/li').click
|
117
|
+
browser.element(xpath: '/html/body/div[1]/div[2]/div[2]/div[1]/div[3]/ul[2]/li[6]/span[2]').click
|
118
|
+
browser.select_list(name: 'TATIWT').select('S')
|
119
|
+
|
120
|
+
browser.textarea(name: 'TATFB').set(tracking_number)
|
121
|
+
browser.button(xpath: '/html/body/div[1]/div[3]/div[3]/div/div[1]/div/button[2]').click
|
122
|
+
|
123
|
+
browser.element(xpath: '/html/body/div[1]/div[3]/div[2]/div/div[1]/div[4]/div[3]/div/table/tbody/tr[2]/td[2]').double_click
|
124
|
+
|
125
|
+
html = browser.element(xpath: '/html/body/div[1]/div[3]/div[2]/div/div/div/form/div[4]/div[2]/div/div/table').inner_html
|
126
|
+
html = Nokogiri::HTML.parse(html)
|
127
|
+
|
128
|
+
url = nil
|
129
|
+
html.css('tr').each do |tr|
|
130
|
+
next unless tr.text.downcase.include?(@conf.dig(:documents, :types, type).downcase)
|
131
|
+
|
132
|
+
link_id = tr.css('td')[1].css('a').to_html.split('id=')[1].split('onfocus')[0].gsub('"', '').strip
|
133
|
+
browser.element(css: "##{link_id}").click
|
134
|
+
url = browser.element(xpath: '/html/body/div[1]/div[3]/div[2]/div/embed').attribute_value('src')
|
135
|
+
end
|
136
|
+
|
137
|
+
browser.close
|
138
|
+
|
139
|
+
raise ReactiveShipping::ResponseError, "API Error: #{@@name}: Document not found" if url.blank?
|
140
|
+
|
141
|
+
download_document(type, tracking_number, url, options)
|
142
|
+
end
|
143
|
+
|
144
|
+
# Rates
|
145
|
+
def build_rate_request(origin, destination, packages, options = {})
|
146
|
+
options = @options.merge(options)
|
147
|
+
|
148
|
+
accessorials = []
|
149
|
+
|
150
|
+
unless options[:accessorials].blank?
|
151
|
+
serviceable_accessorials?(options[:accessorials])
|
152
|
+
options[:accessorials].each do |a|
|
153
|
+
unless @conf.dig(:accessorials, :unserviceable).include?(a)
|
154
|
+
accessorials << { code: @conf.dig(:accessorials, :mappable)[a] }
|
155
|
+
end
|
156
|
+
end
|
157
|
+
end
|
158
|
+
|
159
|
+
accessorials = accessorials.uniq.to_a
|
160
|
+
|
161
|
+
request = {
|
162
|
+
'arg0' => {
|
163
|
+
securityinfo: build_soap_header,
|
164
|
+
quote: {
|
165
|
+
iam: options[:iam].blank? ? 'D' : options[:iam], # S for shipper, C for consignee, D for third party
|
166
|
+
shipper: {
|
167
|
+
city: origin.to_hash[:city].to_s.upcase,
|
168
|
+
state: origin.to_hash[:province].to_s.upcase,
|
169
|
+
zip: origin.to_hash[:postal_code].to_s.upcase
|
170
|
+
},
|
171
|
+
consignee: {
|
172
|
+
city: destination.to_hash[:city].to_s.upcase,
|
173
|
+
state: destination.to_hash[:province].to_s.upcase,
|
174
|
+
zip: destination.to_hash[:postal_code].to_s.upcase
|
175
|
+
},
|
176
|
+
accessorialcount: accessorials.size,
|
177
|
+
accessorial: accessorials.blank? ? [] : accessorials,
|
178
|
+
ppdcol: options[:payment_type].blank? ? 'P' : options[:payment_type].blank?, # Prepaid
|
179
|
+
itemcount: packages.size,
|
180
|
+
item: packages.inject([]) do |arr, package|
|
181
|
+
arr << {
|
182
|
+
_class: package.freight_class,
|
183
|
+
description: 'Freight'.upcase, # Required
|
184
|
+
haz: '', # Y if yes
|
185
|
+
pallets: 1,
|
186
|
+
pieces: 1,
|
187
|
+
weight: package.pounds.ceil
|
188
|
+
}
|
189
|
+
end
|
190
|
+
}
|
191
|
+
}
|
192
|
+
}
|
193
|
+
|
194
|
+
save_request(request)
|
195
|
+
request
|
196
|
+
end
|
197
|
+
|
198
|
+
def parse_rate_response(origin, destination, packages, response)
|
199
|
+
success = true
|
200
|
+
message = ''
|
201
|
+
|
202
|
+
if !response
|
203
|
+
success = false
|
204
|
+
message = 'API Error: Unknown response'
|
205
|
+
else
|
206
|
+
if !response.dig(:getquote_response, :return, :rating, :errorcode).blank?
|
207
|
+
success = false
|
208
|
+
message = response.dig(:getquote_response, :return, :rating, :errorcode)
|
209
|
+
else
|
210
|
+
cost = (response.dig(:getquote_response, :return, :rating, :amount).to_f * 100).to_i
|
211
|
+
|
212
|
+
longest_dimension = packages.inject([]) { |_arr, p| [p.length(:in), p.width(:in)] }.max.ceil
|
213
|
+
if !@options[:tariff].blank?
|
214
|
+
if longest_dimension >= 168
|
215
|
+
cost += @options[:tariff].dig('overlength_fees').dig('over_14_ft')
|
216
|
+
elsif longest_dimension >= 144 && longest_dimension < 168
|
217
|
+
cost += @options[:tariff].dig('overlength_fees').dig('12_through_14_ft')
|
218
|
+
elsif longest_dimension >= 120 && longest_dimension < 144
|
219
|
+
cost += @options[:tariff].dig('overlength_fees').dig('10_through_12_ft')
|
220
|
+
elsif longest_dimension >= 96 && longest_dimension < 120
|
221
|
+
cost += @options[:tariff].dig('overlength_fees').dig('8_through_10_ft')
|
222
|
+
end
|
223
|
+
elsif longest_dimension >= 96
|
224
|
+
warn 'API Warning: Overlength fees not applied because `tariff` is empty!'
|
225
|
+
end
|
226
|
+
|
227
|
+
transit_days = response.dig(
|
228
|
+
:getquote_response,
|
229
|
+
:return,
|
230
|
+
:service,
|
231
|
+
:days
|
232
|
+
).to_i
|
233
|
+
|
234
|
+
# Calculate real transit time based on information we have about the destination service days
|
235
|
+
%i[mon tue wed thu fri].each do |weekday|
|
236
|
+
days += 1 if response.dig(:getquote_response, :return, :service, :destination, weekday) == 'N'
|
237
|
+
end
|
238
|
+
|
239
|
+
estimate_reference = response.dig(
|
240
|
+
:getquote_response,
|
241
|
+
:return,
|
242
|
+
:rating,
|
243
|
+
:quotenumber
|
244
|
+
)
|
245
|
+
|
246
|
+
if cost
|
247
|
+
rate_estimates = [
|
248
|
+
RateEstimate.new(
|
249
|
+
origin,
|
250
|
+
destination,
|
251
|
+
{ scac: self.class.scac.upcase, name: self.class.name },
|
252
|
+
:standard,
|
253
|
+
transit_days: transit_days,
|
254
|
+
estimate_reference: estimate_reference,
|
255
|
+
total_cost: cost,
|
256
|
+
total_price: cost,
|
257
|
+
currency: 'USD',
|
258
|
+
with_excessive_length_fees: @conf.dig(:attributes, :rates, :with_excessive_length_fees)
|
259
|
+
)
|
260
|
+
]
|
261
|
+
else
|
262
|
+
success = false
|
263
|
+
message = 'API Error: Cost is emtpy'
|
264
|
+
end
|
265
|
+
end
|
266
|
+
end
|
267
|
+
|
268
|
+
RateResponse.new(
|
269
|
+
success,
|
270
|
+
message,
|
271
|
+
response,
|
272
|
+
rates: rate_estimates,
|
273
|
+
response: response,
|
274
|
+
request: last_request
|
275
|
+
)
|
276
|
+
end
|
277
|
+
|
278
|
+
# Tracking
|
279
|
+
def build_tracking_request(tracking_number)
|
280
|
+
request = {
|
281
|
+
'arg0' => {
|
282
|
+
securityinfo: build_soap_header,
|
283
|
+
pronumber: tracking_number
|
284
|
+
}
|
285
|
+
}
|
286
|
+
|
287
|
+
save_request(request)
|
288
|
+
request
|
289
|
+
end
|
290
|
+
|
291
|
+
def parse_location(code)
|
292
|
+
case code
|
293
|
+
when 'LA'
|
294
|
+
Location.new(
|
295
|
+
city: 'Los Angeles',
|
296
|
+
province: 'CA',
|
297
|
+
state: 'CA',
|
298
|
+
country: ActiveUtils::Country.find('USA')
|
299
|
+
)
|
300
|
+
else
|
301
|
+
Location.new(
|
302
|
+
city: code,
|
303
|
+
province: nil,
|
304
|
+
state: nil,
|
305
|
+
country: ActiveUtils::Country.find('USA')
|
306
|
+
)
|
307
|
+
end
|
308
|
+
end
|
309
|
+
|
310
|
+
def parse_tracking_response(response)
|
311
|
+
unless response.dig(:tracktrace_response, :return, :currentstatus, :errorcode).blank?
|
312
|
+
status = response.dig(:tracktrace_response, :return, :currentstatus, :errorcode)
|
313
|
+
return TrackingResponse.new(false, status, response, carrier: "#{@@scac}, #{@@name}", xml: response, response: response, request: last_request)
|
314
|
+
end
|
315
|
+
|
316
|
+
receiver_address = Location.new(
|
317
|
+
city: response.dig(:tracktrace_response, :return, :currentstatus, :consignee, :city).titleize,
|
318
|
+
province: response.dig(:tracktrace_response, :return, :currentstatus, :consignee, :state).upcase,
|
319
|
+
state: response.dig(:tracktrace_response, :return, :currentstatus, :consignee, :state).upcase,
|
320
|
+
country: ActiveUtils::Country.find('USA')
|
321
|
+
)
|
322
|
+
|
323
|
+
shipper_address = Location.new(
|
324
|
+
city: response.dig(:tracktrace_response, :return, :currentstatus, :shipper, :city).titleize,
|
325
|
+
province: response.dig(:tracktrace_response, :return, :currentstatus, :shipper, :state).upcase,
|
326
|
+
state: response.dig(:tracktrace_response, :return, :currentstatus, :shipper, :state).upcase,
|
327
|
+
country: ActiveUtils::Country.find('USA')
|
328
|
+
)
|
329
|
+
|
330
|
+
actual_delivery_date = response.dig(:tracktrace_response, :return, :currentstatus, :deliverydate)
|
331
|
+
unless actual_delivery_date.blank?
|
332
|
+
comment = response.dig(:tracktrace_response, :return, :currentstatus, :status).downcase
|
333
|
+
if comment.starts_with?('delivered')
|
334
|
+
actual_delivery_date = parse_date(comment.downcase.split('signed')[0].split('on')[1].strip.sub('at ', ''))
|
335
|
+
end
|
336
|
+
end
|
337
|
+
|
338
|
+
ship_time = parse_date(response.dig(:tracktrace_response, :return, :currentstatus, :shipdate))
|
339
|
+
scheduled_delivery_date = parse_date(response.dig(:tracktrace_response, :return, :currentstatus, :estdeliverydate))
|
340
|
+
tracking_number = response.dig(:tracktrace_response, :return, :pronumber)
|
341
|
+
|
342
|
+
shipment_events = []
|
343
|
+
response.dig(:tracktrace_response, :return, :history).each do |api_event|
|
344
|
+
event = nil
|
345
|
+
@conf.dig(:events, :types).each do |key, val|
|
346
|
+
if api_event.dig(:description).downcase.include? val
|
347
|
+
event = key
|
348
|
+
break
|
349
|
+
end
|
350
|
+
end
|
351
|
+
next if event.blank?
|
352
|
+
|
353
|
+
datetime_without_time_zone = parse_datetime("#{api_event.dig(:date)} #{api_event.dig(:time)}")
|
354
|
+
location = parse_location(api_event.dig(:location))
|
355
|
+
|
356
|
+
# status and type_code set automatically by ActiveFreight based on event
|
357
|
+
shipment_events << ShipmentEvent.new(event, datetime_without_time_zone, location)
|
358
|
+
end
|
359
|
+
|
360
|
+
shipment_events = shipment_events.sort_by(&:time)
|
361
|
+
|
362
|
+
TrackingResponse.new(
|
363
|
+
true,
|
364
|
+
shipment_events.last.status,
|
365
|
+
response,
|
366
|
+
carrier: "#{@@scac}, #{@@name}",
|
367
|
+
xml: response,
|
368
|
+
response: response,
|
369
|
+
status: status,
|
370
|
+
type_code: shipment_events.last.status,
|
371
|
+
ship_time: ship_time,
|
372
|
+
scheduled_delivery_date: scheduled_delivery_date,
|
373
|
+
actual_delivery_date: actual_delivery_date,
|
374
|
+
delivery_signature: nil,
|
375
|
+
shipment_events: shipment_events,
|
376
|
+
shipper_address: shipper_address,
|
377
|
+
origin: shipper_address,
|
378
|
+
destination: receiver_address,
|
379
|
+
tracking_number: tracking_number,
|
380
|
+
request: last_request
|
381
|
+
)
|
382
|
+
end
|
383
|
+
end
|
384
|
+
end
|