snl-peddler 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
data/.gitignore ADDED
@@ -0,0 +1 @@
1
+ .DS_Store
data/History.txt ADDED
@@ -0,0 +1,2 @@
1
+ == 0.1.0 / 2009-08-14
2
+ * First public release.
data/LICENSE ADDED
@@ -0,0 +1,22 @@
1
+ (The MIT License)
2
+
3
+ Copyright (c) 2009 Hakan Şenol Ensari
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ 'Software'), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
19
+ IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
20
+ CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
21
+ TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
22
+ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.rdoc ADDED
@@ -0,0 +1,104 @@
1
+ = Peddler
2
+
3
+ Peddler is a Ruby wrapper to the Amazon Inventory management API.
4
+
5
+ == Example usage
6
+
7
+ Fire off a client:
8
+
9
+ client = Peddler::Client.new(
10
+ :username => "foo@bar.com",
11
+ :password => "secret",
12
+ :region => "us")
13
+
14
+ Create an inventory file:
15
+
16
+ batch = client.new_inventory_batch
17
+ item = client.new_inventory_item(
18
+ :product_id => "1234567890",
19
+ :price => 100.00,
20
+ :sku => "SKU-123",
21
+ :quantity => 10)
22
+ batch << item
23
+ ...
24
+
25
+ Repeat ad infinitum and then upload:
26
+
27
+ batch.upload
28
+
29
+ The batch now should have an upload ID assigned to it. Let's check its error log.
30
+
31
+ upload_log = client.new_report(
32
+ :upload,
33
+ :id => batch.id)
34
+ upload_log.body
35
+ => "Feed Processing Summary:\n\tNumber of records processed\t\t1\n\tNumber of records successful\t\t1\n\n"
36
+
37
+ You're done uploading your inventory and now want to download some reports from Amazon. Let's go ahead and fetch a preorder list for the past two days to see if there is anything you can fill:
38
+
39
+ preorders_report = client.new_report(
40
+ :preorder,
41
+ :product_line => "Books",
42
+ :frequency => 2)
43
+ preorders = Peddler::Handlers::TabDelimitedHandler.decode_response(preorders_report.body)
44
+ p preorders.size
45
+ => 2000
46
+ p preorders[0].asin
47
+ => "1234567890"
48
+ p preorders[0].average_asking_price
49
+ => "100"
50
+
51
+ Maybe you're probably wondering if you have any new orders:
52
+
53
+ orders_report = client.new_report :order
54
+ orders = Peddler::Handlers::TabDelimitedHandler.decode_response(orders_report.body)
55
+ p orders.size
56
+ => 1500
57
+ p orders[0].item_name
58
+ => "A Thousand Plateaus: Capitalism and Schizophrenia (Paperback) by Gilles Deleuze"
59
+
60
+ You have processed the orders and want to post back the results to Amazon, now that they're "charge when ship." Let's start with sending shipment info:
61
+
62
+ feed = client.new_order_fulfillment_feed
63
+ fulfilled_order = new_fulfilled_order(
64
+ :order_id => "123-1234567-1234567",
65
+ :order_date => "2009-08-01",
66
+ :carrier_code => "USPS",
67
+ :tracking_number => "0308 0330 0000 0000 0000")
68
+ feed << fulfilled_order
69
+
70
+ Again, repeat until done and upload:
71
+
72
+ feed.upload
73
+
74
+ Curious to see the processing report?
75
+
76
+ p feed.status
77
+ => "_SUBMITTED_"
78
+
79
+ Refresh until you see:
80
+
81
+ p feed.status!
82
+ => "_DONE_"
83
+
84
+ Finally, check the report:
85
+
86
+ p feed.download.to_s
87
+ => ...
88
+
89
+ Sadly, you also have an order you can't fulfill. No problem. The workflow is quite similar:
90
+
91
+ feed = client.new_order_cancellation_feed
92
+ cancelled_order = new_cancelled_order(
93
+ :order_id => "123-1234567-1234567")
94
+ feed << cancelled_order
95
+ feed.upload
96
+ sleep(60)
97
+ feed.status!
98
+ => "_DONE_"
99
+ p feed.download.to_s
100
+ => ...
101
+
102
+ Run rdoc and check the source for more detailed info.
103
+
104
+ Copyright © 2009 Hakan Senol Ensari, released under the MIT license
data/Rakefile ADDED
@@ -0,0 +1,17 @@
1
+ require "rubygems"
2
+ require "rake"
3
+ begin
4
+ require "jeweler"
5
+ Jeweler::Tasks.new do |s|
6
+ s.name = "peddler"
7
+ s.summary = "Peddler is a Ruby wrapper to the Amazon Inventory Management API."
8
+ s.email = "hakan.ensari@papercavalier.com"
9
+ s.homepage = "http://github.com/snl/peddler"
10
+ s.description = "Peddler is a Ruby wrapper to the Amazon Inventory Management API."
11
+ s.authors = ["Hakan Senol Ensari"]
12
+ s.add_dependency "xmlsimple"
13
+ s.add_development_dependency "rspec"
14
+ end
15
+ rescue LoadError
16
+ puts "Jeweler, or one of its dependencies, is not available. Install it with: sudo gem install technicalpickles-jeweler -s http://gems.github.com"
17
+ end
data/VERSION.yml ADDED
@@ -0,0 +1,4 @@
1
+ ---
2
+ :major: 0
3
+ :minor: 1
4
+ :patch: 0
data/lib/peddler.rb ADDED
@@ -0,0 +1,25 @@
1
+ # Peddler is a Ruby wrapper to the Amazon Inventory management API.
2
+ module Peddler
3
+ VERSION = "0.1"
4
+ end
5
+
6
+ class String #:nodoc: all
7
+ def camelize
8
+ self.gsub(/(^|_)(.)/) { $2.upcase }
9
+ end
10
+ end
11
+
12
+ require "net/https"
13
+ require "ostruct"
14
+ require "tempfile"
15
+ require "time"
16
+ require "xmlsimple"
17
+
18
+ require File.dirname(__FILE__) + "/peddler/client"
19
+ require File.dirname(__FILE__) + "/peddler/handlers"
20
+ require File.dirname(__FILE__) + "/peddler/feeds"
21
+ require File.dirname(__FILE__) + "/peddler/inventory"
22
+ require File.dirname(__FILE__) + "/peddler/legacy_reports"
23
+ require File.dirname(__FILE__) + "/peddler/refunds"
24
+ require File.dirname(__FILE__) + "/peddler/reports"
25
+ require File.dirname(__FILE__) + "/peddler/transport"
@@ -0,0 +1,240 @@
1
+ # = Peddler
2
+ # Peddler is a Ruby wrapper to the Amazon Inventory management API.
3
+ #
4
+ # Peddler::Client has some detailed explanation and examples of usage.
5
+ module Peddler
6
+ # This is the public interface of the Peddler library.
7
+ class Client
8
+ # Creates a client instance.
9
+ #
10
+ # client = Peddler::Client.new :username => "foo@bar.com",
11
+ # :password => "secret",
12
+ # :region => "us"
13
+ #
14
+ def initialize(params={})
15
+ params.each_pair { |key, value| self.send("#{key}=", value) }
16
+ end
17
+
18
+ def username=(username)
19
+ self.transport.username = username
20
+ end
21
+
22
+ def password=(password)
23
+ self.transport.password = password
24
+ end
25
+
26
+ # Sets Amazon region. Works with [ "us", "uk", "de", "ca", "fr", "jp" ].
27
+ def region=(region)
28
+ self.transport.region = region
29
+ end
30
+
31
+ # Creates an inventory batch.
32
+ #
33
+ # A sample workflow:
34
+ #
35
+ # batch = client.new_inventory_batch
36
+ # book = new_inventory_item(
37
+ #  :product_id => "1234567890",
38
+ # :sku => "SKU-001",
39
+ # :price => 10.00,
40
+ # :quantity => 1)
41
+ # batch << book
42
+ # batch.upload
43
+ #
44
+ # The batch should now have an upload id assigned to it by Amazon. Once processed, you can use this to
45
+ # check the error log:
46
+ #
47
+ # report = client.new_report :upload, :id => batch.id
48
+ # report.body
49
+ #
50
+ # That should output something like the following, assuming everything went well with the upload:
51
+ #
52
+ # "Feed Processing Summary:\n\tNumber of records processed\t\t1\n\tNumber of records successful\t\t1\n\n"
53
+ #
54
+ def new_inventory_batch
55
+ Peddler::Inventory::Batch.new(self.transport.dup)
56
+ end
57
+
58
+ # Creates an inventory item. Parameter keys are lowercased and underscored but otherwise the same as
59
+ # Amazon's colum titles in their tab-delimited templates.
60
+ def new_inventory_item(params={})
61
+ Peddler::Inventory::Item.new(params)
62
+ end
63
+
64
+ # Returns count of pending inventory uploads queued at Amazon.
65
+ def inventory_queue
66
+ Peddler::Inventory::Queue.count(self.transport)
67
+ end
68
+
69
+ # Creates an order fulfillment batch.
70
+ #
71
+ # A sample workflow:
72
+ #
73
+ # feed = client.new_order_fulfillment_feed
74
+ # fulfilled_order = new_fulfilled_order(
75
+ # :order_id => "123-1234567-1234567",
76
+ # :order_date => "2009-08-01")
77
+ # feed << fulfilled_order
78
+ # feed.upload
79
+ # feed.status
80
+ # => "_SUBMITTED_"
81
+ #
82
+ # Now, refresh the status until you see:
83
+ #
84
+ # feed.status!
85
+ # => "_DONE_"
86
+ #
87
+ # Finally, check the processing report:
88
+ #
89
+ # feed.download.to_s
90
+ #
91
+ def new_order_fulfillment_feed
92
+ Peddler::Feeds::OrderFulfillment::Batch.new(self.transport.dup)
93
+ end
94
+
95
+ # Creates an item that can then be added to an order fulfillment feed. Keys are lowercased and underscored but
96
+ # otherwise the same as Amazon's headers. See section 7.1 in the API docs.
97
+ def new_fulfilled_order(params={})
98
+ Peddler::Feeds::OrderFulfillment::Item.new(params)
99
+ end
100
+
101
+ # Creates an order cancellation batch.
102
+ #
103
+ # A sample workflow:
104
+ #
105
+ # feed = client.new_order_cancellation_feed
106
+ # cancelled_order = new_cancelled_order(
107
+ # :order_id => "123-1234567-1234567")
108
+ # feed << cancelled_order
109
+ # feed.upload
110
+ # feed.status
111
+ # => "_SUBMITTED_"
112
+ #
113
+ # Now, refresh the status until you see:
114
+ #
115
+ # feed.status!
116
+ # => "_DONE_"
117
+ #
118
+ # Finally, check the processing report:
119
+ #
120
+ # feed.download.to_s
121
+ #
122
+ def new_order_cancellation_feed
123
+ Peddler::Feeds::OrderCancellation::Batch.new(self.transport.dup)
124
+ end
125
+
126
+ # Creates an item that can then be added to an order cancellation feed. Keys are lowercased and underscored but
127
+ # otherwise the same as Amazon's headers. See section 7.4 in the API docs.
128
+ def new_cancelled_order(params={})
129
+ Peddler::Feeds::OrderCancellation::Item.new(params)
130
+ end
131
+
132
+ # Creates a refund batch.
133
+ #
134
+ # batch = client.new_refund_batch
135
+ # refund = client.new_refund(
136
+ # :order_id => "123-1234567-1234567",
137
+ # :payments_transaction_id => "12341234567890",
138
+ # :refund_amount => 10.00,
139
+ # :reason => "CouldNotShip",
140
+ # :message => "With our apologies.")
141
+ # batch << refund
142
+ # batch.upload
143
+ #
144
+ # To follow up on the status of the upload a little while afterwards:
145
+ #
146
+ # status = client.latest_reports :batch_refund, :count => 1
147
+ # report = client.new_report(
148
+ # :batch_refund,
149
+ # :id => status[0].id)
150
+ #
151
+ # Assuming the refund was successfully processed, report.body should now output:
152
+ #
153
+ # "123-1234567-1234567order-item-id: 12341234567890\tSUCCESS 10.00 is Refunded.\r\n"
154
+ #
155
+ def new_refund_batch
156
+ Peddler::Refunds::Batch.new(self.transport.dup)
157
+ end
158
+
159
+ # Creates a refund item that can then be added to a refund batch.
160
+ #
161
+ # Reasons can be [ "GeneralAdjustment" "CouldNotShip" "DifferentItem" "MerchandiseNotReceived" "MerchandiseNotAsDescribed" ].
162
+ def new_refund(params={})
163
+ Peddler::Refunds::Item.new(params)
164
+ end
165
+
166
+ # Creates an instance for an already-generated report. Works only with what I call legacy reports, that is,
167
+ # anything that comes before section 7 in the API docs. Report names can be [ :upload, :order, :preorder, :batch_refund, :open_listings, :open_listings_lite, :open_listings_liter ].
168
+ #
169
+ # You can download a specific report by using its ID. Otherwise, the instance will fetch the latest available report. One
170
+ # oddball exception: upload reports do require an ID and will return nil if you don't provide one.
171
+ #
172
+ # orders_report = client.new_report :order
173
+ # orders = Peddler::Handlers::TabDelimitedHandler.decode_response(orders_report.body)
174
+ # orders[0].buyer_name
175
+ # => "John Doe"
176
+ #
177
+ # preorders_report = client.new_report(
178
+ #  :preorder,
179
+ # :product_line => "Books",
180
+ # :frequency => 2)
181
+ # preorders = Peddler::Handlers::TabDelimitedHandler.decode_response(preorders_report.body)
182
+ # preorders[0].average_asking_price
183
+ # => "100"
184
+ #
185
+ def new_report(name,params={})
186
+ Peddler::LegacyReports::Report.new(self.transport.dup, name, params)
187
+ end
188
+
189
+ # Requests a report. Returns true when successful. Name can be [ :order, :open_listings, :open_listings_lite, :open_listings_liter ].
190
+ #
191
+ # Here's some sample usage:
192
+ #
193
+ # client.generate_report :order, :number_of_days => 15
194
+ #
195
+ # client.generate_report :open_listings
196
+ #
197
+ # A word of caution. Open listings may crap up with larger inventories. I will have to migrate to a cURL-based
198
+ # HTTP client to get that working again.
199
+ def generate_report(name,params={})
200
+ Peddler::LegacyReports.generate(self.transport, name, params)
201
+ end
202
+
203
+ # Creates an unshipped order report. Takes on some optional parameters, such as :id, :starts_at, :ends_at. By default,
204
+ # it will request a new unshipped order report for the past seven days.
205
+ #
206
+ # report = client.new_unshipped_order_report
207
+ # report.status
208
+ # => "_SUBMITTED_"
209
+ #
210
+ # Now, refresh the status until you see:
211
+ #
212
+ # report.status!
213
+ # => "_DONE_"
214
+ #
215
+ # Once done, you'll get a nifty array of unshipped orders:
216
+ #
217
+ # report.unshipped_orders
218
+ #
219
+ def new_unshipped_orders_report(params={})
220
+ Peddler::Reports::UnshippedOrdersReport.new(self.transport.dup, params)
221
+ end
222
+
223
+ # Returns status of most recent reports. Optional "count" defaults to 10. Name can be [ :upload, :order, :batch_refund, :open_listings, :open_listings_lite, :open_listings_liter ].
224
+ #
225
+ # Some sample usage:
226
+ #
227
+ # reports = client.latest_reports :order, :count => 1
228
+ # reports[0]
229
+ # => #<Peddler::LegacyReports::ReportStatus starts_at="07-29-2009:10-00-06" ...
230
+ #
231
+ def latest_reports(name,params={})
232
+ Peddler::LegacyReports.latest(self.transport, name, params)
233
+ end
234
+
235
+ protected
236
+ def transport #:nodoc:all
237
+ @transport ||= Peddler::Transport.new
238
+ end
239
+ end
240
+ end
@@ -0,0 +1,181 @@
1
+ module Peddler
2
+ # This module includes functionality to handle the charge-when-ship-related feeds Amazon added to the API
3
+ # in its latest incarnation in 2009.
4
+ module Feeds
5
+ # Downloadable file. The processing report in case of feeds. Outputs Amazon's response verbatim.
6
+ # Will add functionality to parse the response some time down the road.
7
+ class Download
8
+ attr_accessor :id, :type, :related_reference_id, :available_at, :acknowledged
9
+
10
+ def initialize(transport, params={})
11
+ @mapped_params = {
12
+ "DownloadId" => "id",
13
+ "DownloadType" => "type",
14
+ "RelatedReferenceId" => "related_reference_id",
15
+ "AvailableDate" => "available_at",
16
+ "Acknowledged" => "acknowledged"}
17
+ @transport = transport
18
+ params.each_pair{ |key, value| self.send "#{@mapped_params[key]}=", value }
19
+ end
20
+
21
+ # Retrieves and returns report
22
+ def to_s
23
+ @body ||= download_report
24
+ end
25
+ private
26
+ def download_report
27
+ return nil if @id.nil?
28
+ @transport.modernize_request
29
+ @transport.query_params.merge!({
30
+ "Action" => "download",
31
+ "downloadId" => @id})
32
+ @transport.execute_request
33
+ end
34
+ end
35
+
36
+ # This is the base class.
37
+ class Feed
38
+ attr_writer :file_content
39
+ attr_accessor :batch, :download, :status, :type, :id, :submitted_at, :started_processing_at, :completed_processing_at, :messages_processed, :messages_successful, :messages_with_errors, :messages_with_warnings
40
+
41
+ def initialize(transport)
42
+ @transport = transport
43
+ @batch = []
44
+ @mapped_params = {
45
+ "UploadStatus" => "status",
46
+ "UploadType" => "type",
47
+ "UploadId" => "id",
48
+ "SubmittedDate" => "submitted_at",
49
+ "StartedProcessingDate" => "started_processing_at",
50
+ "CompletedProcessingDate" => "completed_processing_at",
51
+ "CompletedProcesssingDate" => "completed_processing_at",
52
+ "MessagesProcessed" => "messages_processed",
53
+ "MessagesSuccessful" => "messages_successful",
54
+ "MessagesWithErrors" => "messages_with_errors",
55
+ "MessagesWithWarnings" => "messages_with_warnings"}
56
+ end
57
+
58
+ # Returns content of the upload file.
59
+ def file_content
60
+ return @file_content if @file_content
61
+ out = @file_header
62
+ @batch.each{ |item| out << item.to_s }
63
+ @file_content = out
64
+ end
65
+
66
+ # Returns status and will also refresh if not already "done."
67
+ def status!
68
+ return @status if @status.nil? || @status =~ /_DONE_/
69
+ refresh_status
70
+ @status
71
+ end
72
+
73
+ # Uploads batch.
74
+ def upload
75
+ raise PeddlerError.new("Batch already uploaded") unless @id.nil?
76
+ @transport.modernize_request
77
+ @transport.query_params.merge!({
78
+ "Action" => "upload",
79
+ "uploadType" => @type})
80
+ @transport.body = file_content
81
+ res = @transport.execute_request
82
+ process_response(res)
83
+ @status
84
+ end
85
+
86
+ # Adds an item to the batch.
87
+ def <<(item)
88
+ @batch << item
89
+ end
90
+ private
91
+ def refresh_status
92
+ @transport.modernize_request
93
+ @transport.query_params.merge!({
94
+ "Action" => "uploadStatus",
95
+ "uploadId" => @id})
96
+ res = @transport.execute_request
97
+ process_response(res)
98
+ end
99
+
100
+ def process_response(res)
101
+ xml = Peddler::Handlers::XMLHandler.decode_response(res)
102
+ params = Peddler::Handlers::XMLHandler.parse(:upload, xml)
103
+ if params[0]
104
+ params[0].each_pair do |key, value|
105
+ if key == "RelatedDownloadsList"
106
+ params = Peddler::Handlers::XMLHandler.parse(:download, value)
107
+ @download = Peddler::Feeds::Download.new(@transport, params[0])
108
+ else
109
+ self.send "#{@mapped_params[key]}=", value
110
+ end
111
+ end
112
+ end
113
+ end
114
+ end
115
+
116
+ module OrderFulfillment
117
+ # This class contains methods to upload order fulfillment info to Amazon.
118
+ # See sections 7.1 through 7.3 in the API documentation for more detail.
119
+ class Batch < Peddler::Feeds::Feed
120
+ def initialize(transport)
121
+ @file_header = "order-id\torder-item-id\tquantity\tship-date\tcarrier-code\tcarrier-name\ttracking-number\tship-method\r\n"
122
+ @type = "_POST_FLAT_FILE_FULFILLMENT_DATA_"
123
+ super(transport)
124
+ end
125
+ end
126
+
127
+ # This is an order fulfillment item.
128
+ class Item
129
+ attr_accessor :order_id, :order_item_id, :quantity, :ship_date, :carrier_name, :tracking_number, :ship_method
130
+ attr_reader :carrier_code
131
+
132
+ def initialize(params={})
133
+ params.each_pair{ |key, value| send("#{key}=", value) }
134
+ end
135
+
136
+ # Validates when setting carrier code.
137
+ def carrier_code=(carrier_code)
138
+ @carrier_code = carrier_code if %w{USPS UPS FedEx other}.include?(carrier_code)
139
+ end
140
+
141
+ # Outputs a formatted line for the tab-delimited upload file.
142
+ def to_s
143
+ "#{@order_id}\t#{@order_item_id}\t#{@quantity}\t#{@ship_date}\t#{@carrier_code}\t#{@carrier_name}\t#{@tracking_number}\t#{@ship_method}\r\n"
144
+ end
145
+ end
146
+ end
147
+
148
+ # This module contains methods to upload cancelled orders to Amazon.
149
+ # See section 7.4 in the API documentation for more detail.
150
+ module OrderCancellation
151
+ class Batch < Peddler::Feeds::Feed
152
+ def initialize(transport)
153
+ @file_header = "TemplateType=OrderCancellation Version=1.0/1.0.3 This row for Amazon.com use only. Do not modify or delete.\r\n" +
154
+ "order-id\tcancellation-reason-code\tamazon-order-item-code\r\n"
155
+ @type = "_POST_FLAT_FILE_ORDER_ACKNOWLEDGEMENT_DATA_"
156
+ super(transport)
157
+ end
158
+ end
159
+
160
+ # This is a cancelled order item.
161
+ class Item
162
+ attr_accessor :order_id, :amazon_order_item_code
163
+ attr_reader :cancellation_reason_code
164
+
165
+ def initialize(params={})
166
+ params.each_pair{ |key, value| send("#{key}=", value) }
167
+ end
168
+
169
+ # Validates when setting cancellation reason code.
170
+ def cancellation_reason_code=(cancellation_reason_code)
171
+ @cancellation_reason_code = cancellation_reason_code if %w{ BuyerCanceled CustomerExchange CustomerReturn GeneralAdjustment MerchandiseNotReceived NoInventory ShippingAddressUndeliverable }.include?(cancellation_reason_code)
172
+ end
173
+
174
+ # Outputs a formatted line for the tab-delimited upload file.
175
+ def to_s
176
+ "#{@order_id}\t#{@cancellation_reason_code}\t#{@amazon_order_item_code}\r\n"
177
+ end
178
+ end
179
+ end
180
+ end
181
+ end