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
@@ -0,0 +1,317 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ReactiveShipping
|
4
|
+
class CarrierLogistics < ReactiveShipping::Platform
|
5
|
+
REACTIVE_FREIGHT_CARRIER = true
|
6
|
+
|
7
|
+
# Documents
|
8
|
+
def find_bol(tracking_number, options = {})
|
9
|
+
options = @options.merge(options)
|
10
|
+
parse_document_response(:bol, tracking_number, options)
|
11
|
+
end
|
12
|
+
|
13
|
+
def find_pod(tracking_number, options = {})
|
14
|
+
options = @options.merge(options)
|
15
|
+
parse_document_response(:pod, tracking_number, options)
|
16
|
+
end
|
17
|
+
|
18
|
+
# Rates
|
19
|
+
def find_rates(origin, destination, packages, options = {})
|
20
|
+
options = @options.merge(options)
|
21
|
+
origin = Location.from(origin)
|
22
|
+
destination = Location.from(destination)
|
23
|
+
packages = Array(packages)
|
24
|
+
|
25
|
+
params = build_rate_params(origin, destination, packages, options)
|
26
|
+
parse_rate_response(origin, destination, packages, commit(:rates, params: params))
|
27
|
+
end
|
28
|
+
|
29
|
+
# Tracking
|
30
|
+
def find_tracking_info(tracking_number)
|
31
|
+
parse_tracking_response(tracking_number)
|
32
|
+
end
|
33
|
+
|
34
|
+
# protected
|
35
|
+
|
36
|
+
def debug?
|
37
|
+
return false if @options[:debug].blank?
|
38
|
+
|
39
|
+
@options[:debug]
|
40
|
+
end
|
41
|
+
|
42
|
+
def build_url(action, options = {})
|
43
|
+
options = @options.merge(options)
|
44
|
+
scheme = @conf.dig(:api, :use_ssl, action) ? 'https://' : 'http://'
|
45
|
+
url = ''.dup
|
46
|
+
url << "#{scheme}#{@conf.dig(:api, :domain)}#{@conf.dig(:api, :endpoints, action)}"
|
47
|
+
url = url.sub('@CARRIER_CODE@', @conf.dig(:api, :carrier_code)) if url.include?('@CARRIER_CODE@')
|
48
|
+
url << options[:params] unless options[:params].blank?
|
49
|
+
url
|
50
|
+
end
|
51
|
+
|
52
|
+
def commit(action, options = {})
|
53
|
+
options = @options.merge(options)
|
54
|
+
url = build_url(action, params: options[:params])
|
55
|
+
|
56
|
+
response = HTTParty.get(url)
|
57
|
+
response.parsed_response if response&.parsed_response
|
58
|
+
end
|
59
|
+
|
60
|
+
# Documents
|
61
|
+
def parse_document_response(action, tracking_number, options = {})
|
62
|
+
options = @options.merge(options)
|
63
|
+
browser = Watir::Browser.new(:chrome, headless: !debug?)
|
64
|
+
browser.goto(build_url(action))
|
65
|
+
|
66
|
+
browser.text_field(name: 'wlogin').set(@options[:username])
|
67
|
+
browser.text_field(name: 'wpword').set(@options[:password])
|
68
|
+
browser.button(name: 'BtnAction1').click
|
69
|
+
|
70
|
+
browser.frameset.frames[1].text_field(id: 'menuquicktrack').set(tracking_number)
|
71
|
+
browser.browser.frameset.frames[1].button(id: 'menusubmit').click
|
72
|
+
|
73
|
+
if action == :bol
|
74
|
+
element = browser.frameset.frames[1].button(value: 'View Bill Of Lading Image')
|
75
|
+
if element.exists?
|
76
|
+
element.click
|
77
|
+
else
|
78
|
+
browser.close
|
79
|
+
raise ReactiveShipping::ResponseError, "API Error: #{self.class.name}: Document not found"
|
80
|
+
end
|
81
|
+
else
|
82
|
+
element = browser.frameset.frames[1].button(value: 'View Delivery Receipt Image')
|
83
|
+
if element.exists?
|
84
|
+
element.click
|
85
|
+
else
|
86
|
+
browser.close
|
87
|
+
raise ReactiveShipping::ResponseError, "API Error: #{self.class.name}: Document not found"
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
url = nil
|
92
|
+
browser.windows.last.use do
|
93
|
+
url = browser.url
|
94
|
+
if url.include?('viewdoc.php')
|
95
|
+
browser.close
|
96
|
+
raise ReactiveShipping::ResponseError, "API Error: #{self.class.name}: Documnent cannot be downloaded"
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
100
|
+
browser.close
|
101
|
+
|
102
|
+
path = if options[:path].blank?
|
103
|
+
File.join(Dir.tmpdir, "#{self.class.name} #{tracking_number} #{action.to_s.upcase}.pdf")
|
104
|
+
else
|
105
|
+
options[:path]
|
106
|
+
end
|
107
|
+
file = File.new(path, 'w')
|
108
|
+
|
109
|
+
File.open(file.path, 'wb') do |file|
|
110
|
+
URI.parse(url).open do |input|
|
111
|
+
file.write(input.read)
|
112
|
+
end
|
113
|
+
rescue OpenURI::HTTPError
|
114
|
+
raise ReactiveShipping::ResponseError, "API Error: #{self.class.name}: Document not found"
|
115
|
+
end
|
116
|
+
|
117
|
+
File.exist?(path) ? path : false
|
118
|
+
end
|
119
|
+
|
120
|
+
# Tracking
|
121
|
+
def parse_city_state(str)
|
122
|
+
return nil if str.blank?
|
123
|
+
|
124
|
+
Location.new(
|
125
|
+
city: str.split(', ')[0].titleize,
|
126
|
+
state: str.split(', ')[1].upcase,
|
127
|
+
country: ActiveUtils::Country.find('USA')
|
128
|
+
)
|
129
|
+
end
|
130
|
+
|
131
|
+
def parse_city_state_zip(str)
|
132
|
+
return nil if str.blank?
|
133
|
+
|
134
|
+
Location.new(
|
135
|
+
city: str.split(', ')[0].titleize,
|
136
|
+
state: str.split(', ')[1].split(' ')[0].upcase,
|
137
|
+
zip_code: str.split(', ')[1].split(' ')[1],
|
138
|
+
country: ActiveUtils::Country.find('USA')
|
139
|
+
)
|
140
|
+
end
|
141
|
+
|
142
|
+
def parse_date(date)
|
143
|
+
date ? DateTime.strptime(date, '%m/%d/%Y %I:%M %p').to_s(:db) : nil
|
144
|
+
end
|
145
|
+
|
146
|
+
def parse_tracking_response(tracking_number)
|
147
|
+
url = "#{build_url(:track)}wbtn=PRO&wpro1=#{tracking_number}"
|
148
|
+
save_request({ url: url })
|
149
|
+
|
150
|
+
begin
|
151
|
+
response = HTTParty.get(url)
|
152
|
+
if !response.code == 200
|
153
|
+
raise ReactiveShipping::ResponseError, "API Error: #{self.class.name}: HTTP #{response.code}"
|
154
|
+
end
|
155
|
+
rescue StandardError
|
156
|
+
raise ReactiveShipping::ResponseError, "API Error: #{self.class.name}: Unknown response:\n#{response.inspect}"
|
157
|
+
end
|
158
|
+
|
159
|
+
if response.body.downcase.include?('please enter a valid pro')
|
160
|
+
raise ReactiveShipping::ResponseError, "API Error: #{self.class.name}: Invalid tracking number"
|
161
|
+
end
|
162
|
+
|
163
|
+
html = Nokogiri::HTML(response.body)
|
164
|
+
tracking_table = html.css('.newtables2')[0]
|
165
|
+
|
166
|
+
actual_delivery_date = nil
|
167
|
+
receiver_address = nil
|
168
|
+
ship_time = nil
|
169
|
+
shipper_address = nil
|
170
|
+
|
171
|
+
shipment_events = []
|
172
|
+
tracking_table.css('tr').reverse.each do |tr|
|
173
|
+
next if tr.text.include?('shipment status')
|
174
|
+
next if tr.css('td').blank?
|
175
|
+
|
176
|
+
datetime_without_time_zone = "#{tr.css('td')[2].text} #{tr.css('td')[3].text}".squeeze
|
177
|
+
event = tr.css('td')[0].text
|
178
|
+
location = tr.css('td')[1].text
|
179
|
+
|
180
|
+
event_key = nil
|
181
|
+
@conf.dig(:events, :types).each do |key, val|
|
182
|
+
if event.downcase.include?(val) && !event.downcase.include?('estimated')
|
183
|
+
event_key = key
|
184
|
+
break
|
185
|
+
end
|
186
|
+
end
|
187
|
+
next if event_key.blank?
|
188
|
+
|
189
|
+
location = (parse_city_state(location.squeeze) if !location.blank? && location.downcase.include?(','))
|
190
|
+
|
191
|
+
event = event_key
|
192
|
+
datetime_without_time_zone = parse_date(datetime_without_time_zone)
|
193
|
+
|
194
|
+
case event_key
|
195
|
+
when :delivered
|
196
|
+
actual_delivery_date = datetime_without_time_zone
|
197
|
+
receiver_address = location
|
198
|
+
when :picked_up
|
199
|
+
shipper_address = location
|
200
|
+
ship_time = datetime_without_time_zone
|
201
|
+
end
|
202
|
+
|
203
|
+
# status and type_code set automatically by ActiveFreight based on event
|
204
|
+
shipment_events << ShipmentEvent.new(event, datetime_without_time_zone, location)
|
205
|
+
end
|
206
|
+
|
207
|
+
scheduled_delivery_date = nil
|
208
|
+
status = shipment_events.last.status
|
209
|
+
|
210
|
+
shipment_events = shipment_events.sort_by(&:time)
|
211
|
+
|
212
|
+
TrackingResponse.new(
|
213
|
+
true,
|
214
|
+
status,
|
215
|
+
{ html: html.to_s },
|
216
|
+
carrier: "#{self.class.scac}, #{self.class.name}",
|
217
|
+
html: html,
|
218
|
+
response: html.to_s,
|
219
|
+
status: status,
|
220
|
+
type_code: status,
|
221
|
+
ship_time: ship_time,
|
222
|
+
scheduled_delivery_date: scheduled_delivery_date,
|
223
|
+
actual_delivery_date: actual_delivery_date,
|
224
|
+
delivery_signature: nil,
|
225
|
+
shipment_events: shipment_events,
|
226
|
+
shipper_address: shipper_address,
|
227
|
+
origin: shipper_address,
|
228
|
+
destination: receiver_address,
|
229
|
+
tracking_number: tracking_number,
|
230
|
+
request: last_request
|
231
|
+
)
|
232
|
+
end
|
233
|
+
|
234
|
+
# Rates
|
235
|
+
def build_rate_params(origin, destination, packages, options = {})
|
236
|
+
options = @options.merge(options)
|
237
|
+
params = ''.dup
|
238
|
+
params << "xmlv=yes&xmluser=#{@options[:username]}"
|
239
|
+
params << "&xmlpass=#{@options[:password]}"
|
240
|
+
params << "&vozip=#{origin.to_hash[:postal_code]}"
|
241
|
+
params << "&vdzip=#{destination.to_hash[:postal_code]}"
|
242
|
+
|
243
|
+
i = 0
|
244
|
+
packages.each do |package|
|
245
|
+
i += 1 # API starts at 1 (not 0)
|
246
|
+
params << "&wpieces[#{i}]=1"
|
247
|
+
params << "&wpallets[#{i}]=1"
|
248
|
+
params << "&vclass[#{i}]=#{package.freight_class}"
|
249
|
+
params << "&wweight[#{i}]=#{package.pounds.ceil}"
|
250
|
+
end
|
251
|
+
|
252
|
+
accessorials = []
|
253
|
+
unless options[:accessorials].blank?
|
254
|
+
serviceable_accessorials?(options[:accessorials])
|
255
|
+
options[:accessorials].each do |a|
|
256
|
+
unless @conf.dig(:accessorials, :unserviceable).include?(a)
|
257
|
+
accessorials << @conf.dig(:accessorials, :mappable)[a]
|
258
|
+
end
|
259
|
+
end
|
260
|
+
end
|
261
|
+
|
262
|
+
calculated_accessorials = build_calculated_accessorials(packages, origin, destination)
|
263
|
+
accessorials = accessorials + calculated_accessorials unless calculated_accessorials.blank?
|
264
|
+
|
265
|
+
accessorials.uniq!
|
266
|
+
params << accessorials.join unless accessorials.blank?
|
267
|
+
|
268
|
+
save_request({ params: params })
|
269
|
+
params
|
270
|
+
end
|
271
|
+
|
272
|
+
def parse_rate_response(origin, destination, _packages, response)
|
273
|
+
success = true
|
274
|
+
message = ''
|
275
|
+
|
276
|
+
if !response
|
277
|
+
success = false
|
278
|
+
message = 'API Error: Unknown response'
|
279
|
+
else
|
280
|
+
if response['error']
|
281
|
+
success = false
|
282
|
+
message = response['error']
|
283
|
+
else
|
284
|
+
cost = response.dig('ratequote', 'quotetotal').delete(',').delete('.').to_i
|
285
|
+
transit_days = response.dig('ratequote', 'busdays').to_i
|
286
|
+
if cost
|
287
|
+
rate_estimates = [
|
288
|
+
RateEstimate.new(
|
289
|
+
origin,
|
290
|
+
destination,
|
291
|
+
{ scac: self.class.scac.upcase, name: self.class.name },
|
292
|
+
:standard,
|
293
|
+
transit_days: transit_days,
|
294
|
+
estimate_reference: nil,
|
295
|
+
total_price: cost,
|
296
|
+
currency: 'USD',
|
297
|
+
with_excessive_length_fees: @conf.dig(:attributes, :rates, :with_excessive_length_fees)
|
298
|
+
)
|
299
|
+
]
|
300
|
+
else
|
301
|
+
success = false
|
302
|
+
message = 'API Error: Cost is emtpy'
|
303
|
+
end
|
304
|
+
end
|
305
|
+
end
|
306
|
+
|
307
|
+
RateResponse.new(
|
308
|
+
success,
|
309
|
+
message,
|
310
|
+
response.to_hash,
|
311
|
+
rates: rate_estimates,
|
312
|
+
response: response,
|
313
|
+
request: last_request
|
314
|
+
)
|
315
|
+
end
|
316
|
+
end
|
317
|
+
end
|
@@ -0,0 +1,102 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ReactiveShipping
|
4
|
+
class Liftoff < ReactiveShipping::Platform
|
5
|
+
REACTIVE_FREIGHT_CARRIER = true
|
6
|
+
|
7
|
+
JSON_HEADERS = {
|
8
|
+
'Accept': 'application/json',
|
9
|
+
'Content-Type': 'application/json',
|
10
|
+
'charset': 'utf-8'
|
11
|
+
}.freeze
|
12
|
+
|
13
|
+
def requirements
|
14
|
+
%i[email password scope]
|
15
|
+
end
|
16
|
+
|
17
|
+
# Documents
|
18
|
+
|
19
|
+
# Rates
|
20
|
+
|
21
|
+
def show(id)
|
22
|
+
request = build_request(:show, params: "/#{id}")
|
23
|
+
commit(request)
|
24
|
+
end
|
25
|
+
|
26
|
+
# Tracking
|
27
|
+
|
28
|
+
# protected
|
29
|
+
|
30
|
+
def build_url(action, options = {})
|
31
|
+
options = @options.merge(options)
|
32
|
+
url = ''.dup
|
33
|
+
url += "#{base_url}#{@conf.dig(:api, :scopes, options[:scope])}#{@conf.dig(:api, :endpoints, action)}"
|
34
|
+
url = url.sub(@conf.dig(:api, :scopes, options[:scope]), '') if action == :authenticate
|
35
|
+
url += options[:params] unless options[:params].blank?
|
36
|
+
url
|
37
|
+
end
|
38
|
+
|
39
|
+
def build_request(action, options = {})
|
40
|
+
options = @options.merge(options)
|
41
|
+
headers = JSON_HEADERS
|
42
|
+
headers = headers.merge(options[:headers]) unless options[:headers].blank?
|
43
|
+
body = options[:body].to_json unless options[:body].blank?
|
44
|
+
|
45
|
+
unless action == :authenticate
|
46
|
+
set_auth_token
|
47
|
+
headers = headers.merge(token)
|
48
|
+
end
|
49
|
+
|
50
|
+
request = {
|
51
|
+
url: build_url(action, options),
|
52
|
+
headers: headers,
|
53
|
+
method: @conf.dig(:api, :methods, action),
|
54
|
+
body: body
|
55
|
+
}
|
56
|
+
|
57
|
+
save_request(request)
|
58
|
+
request
|
59
|
+
end
|
60
|
+
|
61
|
+
def commit(request)
|
62
|
+
url = request[:url]
|
63
|
+
headers = request[:headers]
|
64
|
+
method = request[:method]
|
65
|
+
body = request[:body]
|
66
|
+
|
67
|
+
response = case method
|
68
|
+
when :post
|
69
|
+
HTTParty.post(url, headers: headers, body: body)
|
70
|
+
else
|
71
|
+
HTTParty.get(url, headers: headers)
|
72
|
+
end
|
73
|
+
|
74
|
+
JSON.parse(response.body) if response&.body
|
75
|
+
end
|
76
|
+
|
77
|
+
def base_url
|
78
|
+
"https://#{@conf.dig(:api, :domain)}#{@conf.dig(:api, :prefix)}#{@conf.dig(:api, :scope, @options[:scope])}"
|
79
|
+
end
|
80
|
+
|
81
|
+
def set_auth_token
|
82
|
+
return @auth_token unless @auth_token.blank?
|
83
|
+
|
84
|
+
request = build_request(
|
85
|
+
:authenticate,
|
86
|
+
body: {
|
87
|
+
email: @options[:email],
|
88
|
+
password: @options[:password]
|
89
|
+
}
|
90
|
+
)
|
91
|
+
|
92
|
+
response = commit(request)
|
93
|
+
@auth_token = response.dig('auth_token')
|
94
|
+
end
|
95
|
+
|
96
|
+
def token
|
97
|
+
{ 'Authorization': "Bearer #{@auth_token}" }
|
98
|
+
end
|
99
|
+
|
100
|
+
# Show
|
101
|
+
end
|
102
|
+
end
|
@@ -0,0 +1,113 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ReactiveShipping
|
4
|
+
class RateEstimate
|
5
|
+
attr_accessor :carrier, :charge_items, :compare_price, :currency,
|
6
|
+
:delivery_category, :delivery_date, :delivery_range,
|
7
|
+
:description, :destination, :estimate_reference, :expires_at,
|
8
|
+
:insurance_price, :messages, :negotiated_rate, :origin,
|
9
|
+
:package_rates, :phone_required, :pickup_time, :service_code,
|
10
|
+
:service_name, :shipment_options, :shipping_date, :transit_days,
|
11
|
+
:with_excessive_length_fees
|
12
|
+
|
13
|
+
def initialize(origin, destination, carrier, service_name, options = {})
|
14
|
+
self.charge_items = options[:charge_items] || []
|
15
|
+
self.compare_price = options[:compare_price]
|
16
|
+
self.currency = options[:currency]
|
17
|
+
self.delivery_category = options[:delivery_category]
|
18
|
+
self.delivery_range = options[:delivery_range]
|
19
|
+
self.description = options[:description]
|
20
|
+
self.estimate_reference = options[:estimate_reference]
|
21
|
+
self.expires_at = options[:expires_at]
|
22
|
+
self.insurance_price = options[:insurance_price]
|
23
|
+
self.messages = options[:messages] || []
|
24
|
+
self.negotiated_rate = options[:negotiated_rate]
|
25
|
+
self.origin = origin
|
26
|
+
self.destination = destination
|
27
|
+
self.carrier = carrier
|
28
|
+
self.service_name = service_name
|
29
|
+
self.package_rates = if options[:package_rates]
|
30
|
+
options[:package_rates].map { |p| p.update(rate: Package.cents_from(p[:rate])) }
|
31
|
+
else
|
32
|
+
Array(options[:packages]).map { |p| { package: p } }
|
33
|
+
end
|
34
|
+
self.phone_required = options[:phone_required]
|
35
|
+
self.pickup_time = options[:pickup_time]
|
36
|
+
self.service_code = options[:service_code]
|
37
|
+
self.shipment_options = options[:shipment_options] || []
|
38
|
+
self.shipping_date = options[:shipping_date]
|
39
|
+
self.transit_days = options[:transit_days]
|
40
|
+
self.total_price = options[:total_price]
|
41
|
+
self.with_excessive_length_fees = options.dig(:with_excessive_length_fees)
|
42
|
+
|
43
|
+
self.delivery_date = @delivery_range.last
|
44
|
+
end
|
45
|
+
|
46
|
+
def total_price
|
47
|
+
@total_price || @package_rates.sum { |pr| pr[:rate] }
|
48
|
+
rescue NoMethodError
|
49
|
+
raise ArgumentError, 'RateEstimate must have a total_price set, or have a full set of valid package rates.'
|
50
|
+
end
|
51
|
+
alias price total_price
|
52
|
+
|
53
|
+
def add(package, rate = nil)
|
54
|
+
cents = Package.cents_from(rate)
|
55
|
+
if cents.nil? && total_price.nil?
|
56
|
+
raise ArgumentError, 'New packages must have valid rate information since this RateEstimate has no total_price set.'
|
57
|
+
end
|
58
|
+
|
59
|
+
@package_rates << { package: package, rate: cents }
|
60
|
+
self
|
61
|
+
end
|
62
|
+
|
63
|
+
def packages
|
64
|
+
package_rates.map { |p| p[:package] }
|
65
|
+
end
|
66
|
+
|
67
|
+
def package_count
|
68
|
+
package_rates.length
|
69
|
+
end
|
70
|
+
|
71
|
+
protected
|
72
|
+
|
73
|
+
def delivery_range=(delivery_range)
|
74
|
+
@delivery_range = delivery_range ? delivery_range.map { |date| date_for(date) }.compact : []
|
75
|
+
end
|
76
|
+
|
77
|
+
def total_price=(total_price)
|
78
|
+
@total_price = Package.cents_from(total_price)
|
79
|
+
end
|
80
|
+
|
81
|
+
def negotiated_rate=(negotiated_rate)
|
82
|
+
@negotiated_rate = negotiated_rate ? Package.cents_from(negotiated_rate) : nil
|
83
|
+
end
|
84
|
+
|
85
|
+
def compare_price=(compare_price)
|
86
|
+
@compare_price = compare_price ? Package.cents_from(compare_price) : nil
|
87
|
+
end
|
88
|
+
|
89
|
+
def currency=(currency)
|
90
|
+
@currency = ActiveUtils::CurrencyCode.standardize(currency)
|
91
|
+
end
|
92
|
+
|
93
|
+
def phone_required=(phone_required)
|
94
|
+
@phone_required = !!phone_required
|
95
|
+
end
|
96
|
+
|
97
|
+
def shipping_date=(shipping_date)
|
98
|
+
@shipping_date = date_for(shipping_date)
|
99
|
+
end
|
100
|
+
|
101
|
+
def insurance_price=(insurance_price)
|
102
|
+
@insurance_price = Package.cents_from(insurance_price)
|
103
|
+
end
|
104
|
+
|
105
|
+
private
|
106
|
+
|
107
|
+
def date_for(date)
|
108
|
+
date && Date.strptime(date.to_s, '%Y-%m-%d')
|
109
|
+
rescue ArgumentError
|
110
|
+
nil
|
111
|
+
end
|
112
|
+
end
|
113
|
+
end
|