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