active_shipping 1.0.0.pre1 → 1.0.0.pre2

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 8664d2221411bccc635ba71eae6bb4262d369753
4
- data.tar.gz: b468c066831f372867d0cd3ecd70ed04069a4d08
3
+ metadata.gz: 73732fbdf2ce8048403b8039027c688126ba5233
4
+ data.tar.gz: 6f16c0d09ecd5392008b5cd6f20a401a0631d8b3
5
5
  SHA512:
6
- metadata.gz: 4e7eb03007d6327ab42ee8aa9af0adfe741542769231354058a2cfbffa40399ef2ab7ab9eaf39c1f57b3e9ae2f12568c8156dd4c49be9769f149e974960f8149
7
- data.tar.gz: 6d3b6f02166d9eed86426e74d1fca59f6737d21dc4410e36c0d1f65eb815d93fbf4587610c6889af02646e3f83f9b37b8df23f4fcddb56b6def3d09659f98b62
6
+ metadata.gz: 2f007d4d627a81ba03b03fc638520e1576087fec52ac0989046720505c8eb550a07ff3501b7e43966624f53d8e0ee6e161d06752c8edeafd8106b91935ff2d42
7
+ data.tar.gz: e56fe6610709fbdd802a3e3bcd573ac3465d97ed3e79a879ea4708520fb43a9658725af24f13a20bd6de0920727db4bbcc5f769ca6a2c61655b106d9c6997874
Binary file
data.tar.gz.sig CHANGED
Binary file
@@ -1,32 +1,45 @@
1
1
  # Contributing to ActiveShipping
2
2
 
3
- We welcome fixes and additions to this project. Please use clean, concise code that follows Ruby community standards. For example:
4
-
5
- * Be consistent
6
- * Don't use too much white space
7
- ** Use 2 space indent, no tabs.
8
- ** No spaces after (, [ and before ],)
9
- * Nor too little
10
- ** Use spaces around operators and after commas, colons and semicolons
11
- ** Indent when as deep as case
12
- * Write lucid code in lieu of adding comments
13
-
14
- ## Contributors
15
-
16
- * James MacAulay (<http://jmacaulay.net>)
17
- * Tobias Luetke (<http://blog.leetsoft.com>)
18
- * Cody Fauser (<http://codyfauser.com>)
19
- * Jimmy Baker (<http://jimmyville.com/>)
20
- * William Lang (<http://williamlang.net/>)
21
- * Cameron Fowler
22
- * Christopher Saunders (<http://christophersaunders.ca>)
23
- * Denis Odorcic
24
- * Dennis O'Connor
25
- * Dennis Theisen
26
- * Edward Ocampo-Gooding
27
- * Isaac Kearse
28
- * John Duff
29
- * Nigel Ramsay
30
- * Philip Arndt
31
- * Vikram Oberoi
32
- * Willem van Bergen
3
+ We welcome fixes and additions to this project. Fork this project, make your changes and submit a pull request!
4
+
5
+ ### Code style
6
+
7
+ Please use clean, concise code that follows Ruby community standards. For example:
8
+
9
+ - Be consistent
10
+ - Don't use too much white space
11
+ - Use 2 space indent, no tabs.
12
+ - No spaces after (, [ and before ],)
13
+ - Nor too little
14
+ - Use spaces around operators and after commas, colons and semicolons
15
+ - Indent when as deep as case
16
+ - Write lucid code in lieu of adding comments
17
+
18
+ ### Pull request guidelines
19
+
20
+ - Add unit tests, and remote tests to make sure we won't introduce regressions to your code later on.
21
+ - Make sure CI passes for all Ruby versions and dependency versions we support.
22
+ - XML handling: use `REXML` for parsing XML, and `builder` to generate it.
23
+ - JSON: use the JSON module that is included in Rubys standard ibrary
24
+ - HTTP: use `ActiveUtils`'s `PostsData`.
25
+ - Do not add new gem dependencies.
26
+
27
+ ### Contributors
28
+
29
+ - James MacAulay (<http://jmacaulay.net>)
30
+ - Tobias Luetke (<http://blog.leetsoft.com>)
31
+ - Cody Fauser (<http://codyfauser.com>)
32
+ - Jimmy Baker (<http://jimmyville.com/>)
33
+ - William Lang (<http://williamlang.net/>)
34
+ - Cameron Fowler
35
+ - Christopher Saunders (<http://christophersaunders.ca>)
36
+ - Denis Odorcic
37
+ - Dennis O'Connor
38
+ - Dennis Theisen
39
+ - Edward Ocampo-Gooding
40
+ - Isaac Kearse
41
+ - John Duff
42
+ - Nigel Ramsay
43
+ - Philip Arndt
44
+ - Vikram Oberoi
45
+ - Willem van Bergen
data/README.md CHANGED
@@ -1,10 +1,9 @@
1
- # Active Shipping
2
-
3
- [![Build Status](https://travis-ci.org/Shopify/active_shipping.png)](https://travis-ci.org/Shopify/active_shipping)
1
+ # ActiveShipping [![Build status](https://travis-ci.org/Shopify/active_shipping.svg?branch=master)](https://travis-ci.org/Shopify/active_shipping)
4
2
 
5
3
  This library interfaces with the web services of various shipping carriers. The goal is to abstract the features that are most frequently used into a pleasant and consistent Ruby API.
6
4
 
7
5
  - Finding shipping rates
6
+ - Registering shipments
8
7
  - Tracking shipments
9
8
 
10
9
  Active Shipping is currently being used and improved in a production environment for [Shopify][]. Development is being done by the Shopify integrations team (<integrations-team@shopify.com>). Discussion is welcome in the [Active Merchant Google Group][discuss].
@@ -100,7 +99,7 @@ Active Shipping is currently being used and improved in a production environment
100
99
 
101
100
  After installing dependencies with `bundle install`, you can run the unit tests with `rake test:units` and the remote tests with `rake test:remote`. The unit tests mock out requests and responses so that everything runs locally, while the remote tests actually hit the carrier servers. For the remote tests, you'll need valid test credentials for any carriers' tests you want to run. The credentials should go in ~/.active_shipping/credentials.yml, and the format of that file can be seen in the included [credentials.yml](https://github.com/Shopify/active_shipping/blob/master/test/credentials.yml).
102
101
 
103
- ## Contributing
102
+ ## Development
104
103
 
105
104
  Yes, please! Take a look at the tests and the implementation of the Carrier class to see how the basics work. At some point soon there will be a carrier template generator along the lines of the gateway generator included in Active Merchant, but carrier.rb outlines most of what's necessary. The other main classes that would be good to familiarize yourself with are Location, Package, and Response.
106
105
 
@@ -116,7 +115,7 @@ To log requests and responses, just set the `logger` on your carrier class to so
116
115
 
117
116
  (This logging functionality is provided by the [`PostsData` module](https://github.com/Shopify/active_utils/blob/master/lib/active_utils/posts_data.rb) in the `active_utils` dependency.)
118
117
 
119
- After you've pushed your well-tested changes to your github fork, make a pull request and we'll take it from there!
118
+ After you've pushed your well-tested changes to your github fork, make a pull request and we'll take it from there! For more information, see CONTRIBUTING.md.
120
119
 
121
120
  ## Legal Mumbo Jumbo
122
121
 
@@ -27,6 +27,9 @@ require 'active_support/core_ext/enumerable'
27
27
  require 'active_support/core_ext/class/attribute_accessors'
28
28
  require 'active_utils'
29
29
 
30
+ require 'rexml/document'
31
+ require 'nokogiri'
32
+
30
33
  require 'vendor/quantified/lib/quantified'
31
34
  require 'vendor/xml_node/lib/xml_node'
32
35
 
@@ -1,7 +1,22 @@
1
1
  module ActiveShipping
2
+
3
+ # Carrier is abstract the base class for all supported carriers.
4
+ #
5
+ # To implement support for a carrier, you should subclass this class and
6
+ # implement all the methods the carrier supports.
7
+ #
8
+ # @see #find_rates
9
+ # @see #create_shipment
10
+ # @see #find_tracking_info
11
+ #
12
+ # @!attribute test_mode
13
+ # Whether to interact with the carrier's sandbox environment.
14
+ # @return [Boolean]
15
+ #
16
+ # @!attribute last_request
17
+ # The last request performed against the carrier's API.
18
+ # @see #save_request
2
19
  class Carrier
3
- include ActiveUtils::RequiresParameters
4
- include ActiveUtils::PostsData
5
20
  include Quantified
6
21
 
7
22
  attr_reader :last_request
@@ -9,6 +24,9 @@ module ActiveShipping
9
24
  alias_method :test_mode?, :test_mode
10
25
 
11
26
  # Credentials should be in options hash under keys :login, :password and/or :key.
27
+ # @param options [Hash] The details needed to connect to the carrier's API.
28
+ # @option options [Boolean] :test Set this to true to connect to the carrier's
29
+ # sandbox environment instead of the production environment.
12
30
  def initialize(options = {})
13
31
  requirements.each { |key| requires!(options, key) }
14
32
  @options = options
@@ -16,22 +34,59 @@ module ActiveShipping
16
34
  @test_mode = @options[:test]
17
35
  end
18
36
 
19
- # Override to return required keys in options hash for initialize method.
20
- def requirements
21
- []
22
- end
23
-
24
- # Override with whatever you need to get the rates
37
+ # Asks the carrier for rate estimates for a given shipment.
38
+ #
39
+ # @note Override with whatever you need to get the rates from the carrier.
40
+ #
41
+ # @param origin [ActiveShipping::Location] Where the shipment will originate from.
42
+ # @param destination [ActiveShipping::Location] Where the package will go.
43
+ # @param packages [Array<ActiveShipping::Package>] The list of packages that will
44
+ # be in the shipment.
45
+ # @param options [Hash] Carrier-specific parameters.
46
+ # @return [ActiveShipping::RateResponse] The response from the carrier, which
47
+ # includes 0 or more rate estimates for different shipping products
25
48
  def find_rates(origin, destination, packages, options = {})
49
+ raise NotImplementedError, "#find_rates is not supported by #{self.class.name}."
26
50
  end
27
51
 
28
- # Override with whatever you need to get a shipping label
52
+ # Registers a new shipment with the carrier, to get a tracking number and
53
+ # potentially shipping labels
54
+ #
55
+ # @note Override with whatever you need to register a shipment, and obtain
56
+ # shipping labels if supported by the carrier.
57
+ #
58
+ # @param origin [ActiveShipping::Location] Where the shipment will originate from.
59
+ # @param destination [ActiveShipping::Location] Where the package will go.
60
+ # @param packages [Array<ActiveShipping::Package>] The list of packages that will
61
+ # be in the shipment.
62
+ # @param options [Hash] Carrier-specific parameters.
63
+ # @return [ActiveShipping::ShipmentResponse] The response from the carrier. This
64
+ # response should include a shipment identifier or tracking_number if successful,
65
+ # and potentially shipping labels.
29
66
  def create_shipment(origin, destination, packages, options = {})
67
+ raise NotImplementedError, "#create_shipment is not supported by #{self.class.name}."
30
68
  end
31
69
 
32
- # Validate credentials with a call to the API. By default this just does a find_rates call
33
- # with the orgin and destination both as the carrier's default_location. Override to provide
34
- # alternate functionality, such as checking for test_mode to use test servers, etc.
70
+ # Retrieves tracking information for a previous shipment
71
+ #
72
+ # @note Override with whatever you need to get a shipping label
73
+ #
74
+ # @param tracking_number [String] The unique identifier of the shipment to track.
75
+ # @param options [Hash] Carrier-specific parameters.
76
+ # @return [ActiveShipping::TrackingResponse] The response from the carrier. This
77
+ # response should a list of shipment tracking events if successful.
78
+ def find_tracking_info(tracking_number, options = {})
79
+ raise NotImplementedError, "#find_tracking_info is not supported by #{self.class.name}."
80
+ end
81
+
82
+ # Validate credentials with a call to the API.
83
+ #
84
+ # By default this just does a `find_rates` call with the orgin and destination both as
85
+ # the carrier's default_location. Override to provide alternate functionality, such as
86
+ # checking for `test_mode` to use test servers, etc.
87
+ #
88
+ # @return [Boolean] Should return `true` if the provided credentials proved to work,
89
+ # `false` otherswise.
35
90
  def valid_credentials?
36
91
  location = self.class.default_location
37
92
  find_rates(location, location, Package.new(100, [5, 15, 30]), :test => test_mode)
@@ -41,17 +96,27 @@ module ActiveShipping
41
96
  true
42
97
  end
43
98
 
99
+ # The maximum weight the carrier will accept.
100
+ # @return [Quantified::Mass]
44
101
  def maximum_weight
45
102
  Mass.new(150, :pounds)
46
103
  end
47
104
 
48
105
  protected
49
106
 
50
- def node_text_or_nil(xml_node)
51
- xml_node ? xml_node.text : nil
107
+ include ActiveUtils::RequiresParameters
108
+ include ActiveUtils::PostsData
109
+
110
+ # Returns the keys that are required to be passed to the options hash
111
+ # @note Override to return required keys in options hash for initialize method.
112
+ # @return [Array<Symbol>]
113
+ def requirements
114
+ []
52
115
  end
53
116
 
54
- # Override in subclasses for non-U.S.-based carriers.
117
+ # The default location to use for {#valid_credentials?}.
118
+ # @note Override for non-U.S.-based carriers.
119
+ # @return [ActiveShipping::Location]
55
120
  def self.default_location
56
121
  Location.new( :country => 'US',
57
122
  :state => 'CA',
@@ -63,11 +128,16 @@ module ActiveShipping
63
128
  :fax => '1-310-275-8159')
64
129
  end
65
130
 
66
- # Use after building the request to save for later inspection. Probably won't ever be overridden.
131
+ # Use after building the request to save for later inspection.
132
+ # @return [void]
67
133
  def save_request(r)
68
134
  @last_request = r
69
135
  end
70
136
 
137
+ # Calculates a timestamp that corresponds a given number if business days in the future
138
+ #
139
+ # @param [Integer] The number of business days from now.
140
+ # @return [DateTime] A timestamp, the provided number of business days in the future.
71
141
  def timestamp_from_business_day(days)
72
142
  return unless days
73
143
  date = DateTime.now.utc
@@ -23,7 +23,8 @@ module ActiveShipping
23
23
  PostalOutlet = Struct.new(:sequence_no, :distance, :name, :business_name, :postal_address, :business_hours)
24
24
 
25
25
  URL = "http://sellonline.canadapost.ca:30000"
26
- DOCTYPE = '<!DOCTYPE eparcel SYSTEM "http://sellonline.canadapost.ca/DevelopersResources/protocolV3/eParcel.dtd">'
26
+ DTD_NAME = 'eparcel'
27
+ DTD_URI = "http://sellonline.canadapost.ca/DevelopersResources/protocolV3/eParcel.dtd"
27
28
 
28
29
  RESPONSE_CODES = {
29
30
  '1' => "All calculation was done",
@@ -106,92 +107,96 @@ module ActiveShipping
106
107
 
107
108
  private
108
109
 
110
+ def generate_xml(&block)
111
+ builder = Nokogiri::XML::Builder.new do |xml|
112
+ xml.doc.create_internal_subset(DTD_NAME, nil, DTD_URI)
113
+ yield(xml)
114
+ end
115
+ builder.to_xml
116
+ end
117
+
109
118
  def build_rate_request(origin, destination, line_items = [], options = {})
110
- line_items = [line_items] unless line_items.is_a?(Array)
111
- origin = origin.is_a?(Location) ? origin : Location.new(origin)
119
+ line_items = [line_items] unless line_items.is_a?(Array)
120
+ origin = origin.is_a?(Location) ? origin : Location.new(origin)
112
121
  destination = destination.is_a?(Location) ? destination : Location.new(destination)
113
122
 
114
- xml_request = XmlNode.new('eparcel') do |root_node|
115
- root_node << XmlNode.new('language', @options[:french] ? 'fr' : 'en')
116
- root_node << XmlNode.new('ratesAndServicesRequest') do |request|
117
-
118
- request << XmlNode.new('merchantCPCID', @options[:login])
119
- request << XmlNode.new('fromPostalCode', origin.postal_code)
120
- request << XmlNode.new('turnAroundTime', options[:turn_around_time]) if options[:turn_around_time]
121
- request << XmlNode.new('itemsPrice', dollar_amount(line_items.map(&:value).compact.sum))
122
-
123
- # line items
124
- request << build_line_items(line_items)
125
-
126
- # delivery info
127
- # NOTE: These tags MUST be after line items
128
- request << XmlNode.new('city', destination.city)
129
- request << XmlNode.new('provOrState', destination.province)
130
- request << XmlNode.new('country', handle_non_iso_country_names(destination.country))
131
- request << XmlNode.new('postalCode', destination.postal_code)
123
+ generate_xml do |xml|
124
+ xml.eparcel do
125
+ xml.language(@options[:french] ? 'fr' : 'en')
126
+ xml.ratesAndServicesRequest do
127
+ xml.merchantCPCID(@options[:login])
128
+ xml.fromPostalCode(origin.postal_code)
129
+ xml.turnAroundTime(options[:turn_around_time]) if options[:turn_around_time]
130
+ xml.itemsPrice(dollar_amount(line_items.map(&:value).compact.sum))
131
+
132
+ build_line_items(xml, line_items)
133
+
134
+ xml.city(destination.city)
135
+ xml.provOrState(destination.province)
136
+ xml.country(handle_non_iso_country_names(destination.country))
137
+ xml.postalCode(destination.postal_code)
138
+ end
132
139
  end
133
140
  end
134
-
135
- DOCTYPE + xml_request.to_s
136
141
  end
137
142
 
138
143
  def parse_rate_response(response, origin, destination, options = {})
139
- xml = REXML::Document.new(response)
144
+ xml = Nokogiri.XML(response)
140
145
  success = response_success?(xml)
141
146
  message = response_message(xml)
142
147
 
143
148
  rate_estimates = []
144
149
  boxes = []
145
150
  if success
146
- xml.elements.each('eparcel/ratesAndServicesResponse/product') do |product|
147
- service_name = (@options[:french] ? @@name_french : @@name) + " " + product.get_text('name').to_s
148
- service_code = product.attribute('id').to_s
151
+ xml.xpath('eparcel/ratesAndServicesResponse/product').each do |product|
152
+ service_name = (@options[:french] ? @@name_french : @@name) + " " + product.at('name').text
153
+ service_code = product['id']
149
154
 
150
155
  rate_estimates << RateEstimate.new(origin, destination, @@name, service_name,
151
156
  :service_code => service_code,
152
- :total_price => product.get_text('rate').to_s,
157
+ :total_price => product.at('rate').text,
153
158
  :currency => 'CAD',
154
- :shipping_date => product.get_text('shippingDate').to_s,
155
- :delivery_range => [product.get_text('deliveryDate').to_s] * 2
159
+ :shipping_date => product.at('shippingDate').text,
160
+ :delivery_range => [product.at('deliveryDate').text] * 2
156
161
  )
157
162
  end
158
163
 
159
- boxes = xml.elements.collect('eparcel/ratesAndServicesResponse/packing/box') do |box|
164
+ boxes = xml.xpath('eparcel/ratesAndServicesResponse/packing/box').map do |box|
160
165
  b = Box.new
161
166
  b.packedItems = []
162
- b.name = box.get_text('name').to_s
163
- b.weight = box.get_text('weight').to_s.to_f
164
- b.expediter_weight = box.get_text('expediterWeight').to_s.to_f
165
- b.length = box.get_text('length').to_s.to_f
166
- b.width = box.get_text('width').to_s.to_f
167
- b.height = box.get_text('height').to_s.to_f
168
- b.packedItems = box.elements.collect('packedItem') do |item|
167
+ b.name = box.at('name').text
168
+ b.weight = box.at('weight').text.to_f
169
+ b.expediter_weight = box.at('expediterWeight').text.to_f
170
+ b.length = box.at('length').text.to_f
171
+ b.width = box.at('width').text.to_f
172
+ b.height = box.at('height').text.to_f
173
+ b.packedItems = box.xpath('packedItem').map do |item|
169
174
  p = PackedItem.new
170
- p.quantity = item.get_text('quantity').to_s.to_i
171
- p.description = item.get_text('description').to_s
175
+ p.quantity = item.at('quantity').text.to_i
176
+ p.description = item.at('description').text
172
177
  p
173
178
  end
174
179
  b
175
180
  end
176
181
 
177
- postal_outlets = xml.elements.collect('eparcel/ratesAndServicesResponse/nearestPostalOutlet') do |outlet|
182
+ postal_outlets = xml.xpath('eparcel/ratesAndServicesResponse/nearestPostalOutlet').map do |outlet|
178
183
  postal_outlet = PostalOutlet.new
179
- postal_outlet.sequence_no = outlet.get_text('postalOutletSequenceNo').to_s
180
- postal_outlet.distance = outlet.get_text('distance').to_s
181
- postal_outlet.name = outlet.get_text('outletName').to_s
182
- postal_outlet.business_name = outlet.get_text('businessName').to_s
184
+ postal_outlet.sequence_no = outlet.at('postalOutletSequenceNo').text
185
+ postal_outlet.distance = outlet.at('distance').text
186
+ postal_outlet.name = outlet.at('outletName').text
187
+ postal_outlet.business_name = outlet.at('businessName').text
183
188
 
184
189
  postal_outlet.postal_address = Location.new(
185
- :address1 => outlet.get_text('postalAddress/addressLine').to_s,
186
- :postal_code => outlet.get_text('postalAddress/postal_code').to_s,
187
- :city => outlet.get_text('postalAddress/municipality').to_s,
188
- :province => outlet.get_text('postalAddress/province').to_s,
190
+ :address1 => outlet.at('postalAddress/addressLine').text,
191
+ :postal_code => outlet.at('postalAddress/postal_code').text,
192
+ :city => outlet.at('postalAddress/municipality').text,
193
+ :province => outlet.at('postalAddress/province').text,
189
194
  :country => 'Canada',
190
- :phone_number => outlet.get_text('phoneNumber').to_s
195
+ :phone_number => outlet.at('phoneNumber').text
191
196
  )
192
197
 
193
198
  postal_outlet.business_hours = outlet.elements.collect('businessHours') do |hour|
194
- { :day_of_week => hour.get_text('dayOfWeek').to_s, :time => hour.get_text('time').to_s }
199
+ { :day_of_week => hour.at('dayOfWeek').text, :time => hour.at('time').text }
195
200
  end
196
201
 
197
202
  postal_outlet
@@ -202,17 +207,17 @@ module ActiveShipping
202
207
  end
203
208
 
204
209
  def response_success?(xml)
205
- return false unless xml.get_text('eparcel/error').nil?
210
+ return false unless xml.at('eparcel/error').nil?
206
211
 
207
- value = xml.get_text('eparcel/ratesAndServicesResponse/statusCode').to_s
212
+ value = xml.at('eparcel/ratesAndServicesResponse/statusCode').text
208
213
  value == '1' || value == '2'
209
214
  end
210
215
 
211
216
  def response_message(xml)
212
217
  if response_success?(xml)
213
- xml.get_text('eparcel/ratesAndServicesResponse/statusMessage').to_s
218
+ xml.at('eparcel/ratesAndServicesResponse/statusMessage').text
214
219
  else
215
- xml.get_text('eparcel/error/statusMessage').to_s
220
+ xml.at('eparcel/error/statusMessage').text
216
221
  end
217
222
  end
218
223
 
@@ -225,17 +230,17 @@ module ActiveShipping
225
230
  # <!-- - description (mandatory) -->
226
231
  # <!-- - ready to ship (optional) -->
227
232
 
228
- def build_line_items(line_items)
229
- XmlNode.new('lineItems') do |line_items_node|
233
+ def build_line_items(xml, line_items)
234
+ xml.lineItems do
230
235
  line_items.each do |line_item|
231
- line_items_node << XmlNode.new('item') do |item|
232
- item << XmlNode.new('quantity', 1)
233
- item << XmlNode.new('weight', line_item.kilograms)
234
- item << XmlNode.new('length', line_item.cm(:length).to_s)
235
- item << XmlNode.new('width', line_item.cm(:width).to_s)
236
- item << XmlNode.new('height', line_item.cm(:height).to_s)
237
- item << XmlNode.new('description', line_item.options[:description] || ' ')
238
- item << XmlNode.new('readyToShip', line_item.options[:ready_to_ship] || nil)
236
+ xml.item do
237
+ xml.quantity(1)
238
+ xml.weight(line_item.kilograms)
239
+ xml.length(line_item.cm(:length).to_s)
240
+ xml.width(line_item.cm(:width).to_s)
241
+ xml.height(line_item.cm(:height).to_s)
242
+ xml.description(line_item.options[:description] || ' ')
243
+ xml.readyToShip(line_item.options[:ready_to_ship] || nil)
239
244
  # By setting the 'readyToShip' tag to true, Sell Online will not pack this item in the boxes defined in the merchant profile.
240
245
  end
241
246
  end