snl-peddler 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,58 @@
1
+ module Peddler
2
+ module Handlers
3
+ class XMLHandler
4
+ # Decodes an XML response.
5
+ def self.decode_response(res)
6
+ XmlSimple.xml_in(res)
7
+ end
8
+ # Parses responses to uploads and status queries for feeds in Section 7 of the docs. Walks
9
+ # through lists and returns an array of hashes.
10
+ def self.parse(name, xml)
11
+ name = name.to_s.capitalize
12
+ list = xml["#{name}sStatusList"] || xml["#{name}sList"]
13
+ if list
14
+ list.collect { |s| parse_status(name, s) }
15
+ else
16
+ [ parse_status(name, xml) ]
17
+ end
18
+ end
19
+
20
+ # Parses legacy responses to queries on statuses of generated reports and inventory uploads.
21
+ def self.parse_legacy(xml)
22
+ if xml["Batch"]
23
+ xml["Batch"].collect { |input| Peddler::LegacyReports::UploadStatus.new(input) }
24
+ elsif xml["Report"]
25
+ xml["Report"].collect { |input| Peddler::LegacyReports::ReportStatus.new(input) }
26
+ end
27
+ end
28
+ protected
29
+ def self.parse_status(name, xml)
30
+ if xml[name]
31
+ xml[name][0].inject({}) do |memo, pair|
32
+ key, value = pair
33
+ value[0] = Time.parse(value[0]) if key =~ /Date$/
34
+ memo.merge!({ key => value[0] })
35
+ end
36
+ end
37
+ end
38
+ end
39
+
40
+ class TabDelimitedHandler
41
+ # Parses tab-delimited content, returning an array of OpenStruct objects corresponding to the rows in the former.
42
+ def self.decode_response(res)
43
+ lines = res.split("\n")
44
+ if lines.size > 1
45
+ params = lines[0].split("\t").collect{ |value| value.gsub(/-/, "_") }
46
+ params_size = params.size
47
+ (1..(lines.size - 1)).collect do |line_key|
48
+ values = lines[line_key].split("\t")
49
+ data = (0..(params_size - 1)).inject({}) { |memo, key| memo.merge( { params[key] => values[key] } ) }
50
+ OpenStruct.new(data)
51
+ end
52
+ else
53
+ res
54
+ end
55
+ end
56
+ end
57
+ end
58
+ end
@@ -0,0 +1,108 @@
1
+ module Peddler
2
+ module Inventory
3
+ module Queue
4
+ # Returns number of inventory uploads queued at Amazon
5
+ def self.count(transport)
6
+ transport.legacize_request
7
+ transport.path << "manual-reports/get-pending-uploads-count"
8
+ res = transport.execute_request
9
+ Peddler::Handlers::XMLHandler.decode_response(res).to_i
10
+ end
11
+ end
12
+
13
+ # This is an inventory batch.
14
+ class Batch
15
+ attr_reader :id
16
+ attr_accessor :batch
17
+
18
+ def initialize(transport)
19
+ @transport = transport
20
+ @batch = []
21
+ end
22
+
23
+ # Uploads batch to Amazon.
24
+ def upload(params={})
25
+ raise PeddlerError.new("Batch already uploaded") unless @id.nil?
26
+ @transport.legacize_request
27
+ @transport.path << "catalog-upload/"
28
+ case params[:method].to_s
29
+ when "modify"
30
+ @transport.path << "modify-only"
31
+ @transport.body = file_content(:short)
32
+ when "purge"
33
+ @transport.path << "purge-replace"
34
+ @transport.body = file_content
35
+ else
36
+ @transport.path << "add-modify-delete"
37
+ @transport.body = file_content
38
+ end
39
+ params.delete(:method)
40
+ params = defaultize(params)
41
+ @transport.headers.merge!(params)
42
+ res = @transport.execute_request
43
+ if res =~ /^<BatchID>(.*)<\/BatchID>$/
44
+ @id = $1
45
+ else
46
+ @id = 0
47
+ end
48
+ true
49
+ end
50
+
51
+ # Reformats parameters and mixes in some defaults.
52
+ def defaultize(params)
53
+ { :upload_for => "Marketplace",
54
+ :file_format =>"TabDelimited",
55
+ :asin_match_create => "Y",
56
+ :asinate => "Y",
57
+ :batch_id => "Y",
58
+ :email => "Y"}.each_pair{ |key, value| params[key] = value unless params[key] }
59
+ # Some Amazon dimwit figured he'd spell this differently
60
+ if params[:enable_expedited_shipping]
61
+ params["enable-expedited-shipping"] = params[:enable_expedited_shipping]
62
+ params.delete(:enable_expedited_shipping)
63
+ else
64
+ params["enable-expedited-shipping"] = "Y"
65
+ end
66
+ params
67
+ end
68
+
69
+ def file_content(type=:long)
70
+ return @file_content if @file_content
71
+ case type
72
+ when :long
73
+ out = "product-id\tproduct-id-type\titem-condition\tprice\tsku\tquantity\tadd-delete\twill-ship-internationally\texpedited-shipping\titem-note\titem-is-marketplace\tfulfillment-center-id\titem-name\titem-description\tcategory1\timage-url\tshipping-fee\tbrowse-path\tstorefront-feature\tboldface\tasin1\tasin2\tasin3\r\n"
74
+ @batch.each{ |item| out << item.to_s }
75
+ when :short
76
+ out = "sku\tprice\tquantity\r\n"
77
+ @batch.each{ |item| out << item.to_s(:short) }
78
+ end
79
+ @file_content = out
80
+ end
81
+
82
+ def file_content=(file_content)
83
+ @file_content = file_content
84
+ end
85
+
86
+ def <<(item)
87
+ @batch << item
88
+ end
89
+ end
90
+
91
+ class Item
92
+ attr_accessor :product_id, :product_id_type, :item_condition, :price, :sku, :quantity, :add_delete, :will_ship_internationally, :expedited_shipping, :item_note, :item_is_marketplace, :fulfillment_center_id, :item_name, :item_description, :category1, :image_url, :shipping_fee, :browse_path, :storefront_feature, :boldface, :asin1, :asin2, :asin3
93
+
94
+ def initialize(params={})
95
+ params.each_pair{ |key, value| send("#{key.to_s}=", value) }
96
+ end
97
+
98
+ def to_s(type=:long)
99
+ case type
100
+ when :long
101
+ "#{self.product_id}\t#{self.product_id_type}\t#{self.item_condition}\t#{self.price}\t#{self.sku}\t#{self.quantity}\t#{self.add_delete}\t#{self.will_ship_internationally}\t#{self.expedited_shipping}\t#{self.item_note}\t#{self.item_is_marketplace}\t#{self.fulfillment_center_id}\t#{self.item_name}\t#{self.item_description}\t#{self.category1}\t#{self.image_url}\t#{self.shipping_fee}\t#{self.browse_path}\t#{self.storefront_feature}\t#{self.boldface}\t#{self.asin1}\t#{self.asin2}\t#{self.asin3}\r\n"
102
+ when :short
103
+ "#{self.sku}\t#{self.price}\t#{self.quantity}\r\n"
104
+ end
105
+ end
106
+ end
107
+ end
108
+ end
@@ -0,0 +1,107 @@
1
+ module Peddler
2
+ # This module contains methods to manage legacy reports -- anything that comes before section 7 in the API docs.
3
+ module LegacyReports
4
+ # Returns statuses of most recent reports in an array of OpenStructs.
5
+ def self.latest(transport,name,params={})
6
+ transport.legacize_request
7
+ if name == :upload
8
+ transport.path << "catalog-upload/get-batches"
9
+ transport.headers[:number_of_batches] = params[:count] if params[:count]
10
+ else
11
+ transport.path << "manual-reports/get-report-status"
12
+ transport.headers[:report_name] = name.to_s.camelize
13
+ transport.headers[:number_of_reports] = params[:count] if params[:count]
14
+ end
15
+ res = transport.execute_request
16
+ xml = Peddler::Handlers::XMLHandler.decode_response(res)
17
+ Peddler::Handlers::XMLHandler.parse_legacy(xml)
18
+ end
19
+
20
+ # Requests a report to be generated and returns the report instance if request is successful.
21
+ def self.generate(transport,name,params={})
22
+ transport.legacize_request
23
+ transport.path << "manual-reports/generate-report-now"
24
+ transport.headers[:report_name] = name.to_s.camelize
25
+ transport.headers.merge!(params)
26
+ res = transport.execute_request
27
+ res =~ /SUCCESS/ ? Peddler::LegacyReports::Report.new(transport, name) : false
28
+ end
29
+
30
+ # A legacy report
31
+ class Report
32
+ attr_accessor :name, :id, :product_line, :frequency
33
+
34
+ def initialize(transport, name=nil, params={})
35
+ @transport, @name = transport, name
36
+ params.each_pair{ |key, value| self.send "#{key}=", value }
37
+ end
38
+
39
+ def body
40
+ return nil if @name == :upload && @id.nil?
41
+ @body ||= download
42
+ end
43
+
44
+ private
45
+ def download
46
+ return false if @name.nil? && @id.nil?
47
+ case @name.to_s
48
+ when "upload"
49
+ @transport.legacize_request
50
+ @transport.path << "download/errorlog"
51
+ @transport.headers["BatchID"] = @id
52
+ @transport.execute_request
53
+ else
54
+ @transport.legacize_request
55
+ @transport.path << "download/report"
56
+ if @id.nil?
57
+ @transport.headers[:report_name] = @name.to_s.camelize
58
+ if @name == :preorder
59
+ @transport.headers["productline"] = @product_line if @product_line
60
+ @transport.headers["frequency"] = @frequency if @frequency
61
+ end
62
+ else
63
+ @transport.headers["ReportID"] = @id
64
+ end
65
+ @transport.execute_request
66
+ end
67
+ end
68
+ end
69
+
70
+ class Status < OpenStruct
71
+ def initialize(input)
72
+ if input.kind_of? String
73
+ hash = input.scan(/([a-z]+)=([^=]+)($| )/).inject({}){ |memo, value| memo.merge( { @keymap[value[0]] => value[1].strip }) }
74
+ end
75
+ super(hash)
76
+ end
77
+
78
+ def id
79
+ @table[:id] || self.object_id
80
+ end
81
+ end
82
+
83
+ class ReportStatus < Status
84
+ def initialize(input)
85
+ @keymap = {
86
+ "reportstarttime" => "starts_at",
87
+ "reportendtime" => "ends_at",
88
+ "reportid" => "id"}
89
+ super(input)
90
+ end
91
+ end
92
+
93
+ class UploadStatus < Status
94
+ def initialize(input)
95
+ @keymap = {
96
+ "status" => "status",
97
+ "batchid" => "id",
98
+ "numberofwarnings" => "number_of_warnings",
99
+ "activateditems" => "activated_items",
100
+ "itemsnotacivated" => "items_not_activated",
101
+ "itemsnotactivated" => "items_not_activated",
102
+ "dateandtime" => "datetime"}
103
+ super(input)
104
+ end
105
+ end
106
+ end
107
+ end
@@ -0,0 +1,54 @@
1
+ module Peddler
2
+ module Refunds
3
+ # This is a refund batch.
4
+ class Batch
5
+ attr_accessor :batch
6
+
7
+ def initialize(transport)
8
+ @transport = transport
9
+ @batch = []
10
+ end
11
+
12
+ def file_content
13
+ out = "order-id\tpayments-transaction-id\trefund-amount\treason\tmessage\r\n"
14
+ @file_content = @batch.inject(out){ |memo, item| memo << item.to_s }
15
+ end
16
+
17
+ def <<(item)
18
+ @batch << item
19
+ end
20
+
21
+ def upload
22
+ raise PeddlerError.new("Batch already uploaded") if @completed
23
+ @transport.legacize_request
24
+ @transport.path << "catalog-upload/batch-refund"
25
+ @transport.body = file_content
26
+ res = @transport.execute_request
27
+ @completed = true if res == "<Success>SUCCESS</Success>"
28
+ end
29
+
30
+ end
31
+
32
+ # This is a refund.
33
+ class Item
34
+ REFUND_REASONS = %w{ GeneralAdjustment CouldNotShip DifferentItem MerchandiseNotReceived MerchandiseNotAsDescribed }
35
+
36
+ attr_accessor :order_id, :payments_transaction_id, :refund_amount, :message
37
+ attr_reader :reason
38
+
39
+ def initialize(options={})
40
+ options.each_pair{ |key, value| send("#{key.to_s}=", value) }
41
+ end
42
+
43
+ def reason=(reason)
44
+ @reason = reason if REFUND_REASONS.include?(reason)
45
+ end
46
+
47
+ def to_s
48
+ "#{self.order_id}\t#{self.payments_transaction_id}\t#{self.refund_amount}\t#{self.reason}\t#{self.message}\r\n"
49
+ end
50
+ end
51
+
52
+ end
53
+
54
+ end
@@ -0,0 +1,94 @@
1
+ module Peddler
2
+ # This module generates and downloads unshipped order reports.
3
+ # I decided to keep this out of Peddler::LegacyReports because the API is quite different.
4
+ module Reports
5
+ # This is an unshipped orders report. It's very similar to the feed objects so I'm just porting over the class.
6
+ # It will have a few stray attributes, but whatever.
7
+ class UnshippedOrdersReport < Peddler::Feeds::Feed
8
+ alias :unshipped_orders :batch
9
+ attr_accessor :starts_at, :ends_at, :scheduled
10
+
11
+ # Creates new unshipped order report. It literally sends a request to Amazon to generate the
12
+ # report if the report ID is not already set.
13
+ def initialize(transport, params={})
14
+ super(transport)
15
+ @mapped_params = {
16
+ "ReportID" => "id",
17
+ "StartDate" => "starts_at",
18
+ "EndDate" => "ends_at",
19
+ "DownloadType" => "type",
20
+ "Scheduled" => "scheduled",
21
+ "ReportStatus" => "status",
22
+ "SubmittedDate" => "submitted_at",
23
+ "StartedProcessingDate" => "started_processing_at",
24
+ "CompletedProcessingDate" => "completed_processing_at",
25
+ "CompletedProcesssingDate" => "completed_processing_at"}
26
+ params.each_pair{ |key, value| self.send "#{key}=", value }
27
+ @starts_at ||= (Date.today - 7).strftime("%Y-%m-%dT00:00:00-00:00")
28
+ @ends_at ||= (Date.today + 1).strftime("%Y-%m-%dT00:00:00-00:00")
29
+ #@type ||= "_GET_CONVERGED_FLAT_FILE_ACTIONABLE_ORDER_DATA_"
30
+ @type ||= "_GET_FLAT_FILE_ACTIONABLE_ORDER_DATA_"
31
+ if @id.nil?
32
+ generate_report
33
+ end
34
+ self
35
+ end
36
+ private
37
+ def refresh_status
38
+ @transport.modernize_request
39
+ @transport.query_params.merge!({
40
+ "Action" => "reportStatus",
41
+ "reportId" => @id})
42
+ res = @transport.execute_request
43
+ process_response(res)
44
+ end
45
+
46
+ def generate_report
47
+ @transport.modernize_request
48
+ @transport.query_params.merge!({
49
+ "Action" => "generateReport",
50
+ "startDate" => @starts_at,
51
+ "endDate" => @ends_at,
52
+ "downloadType" => @type})
53
+ res = @transport.execute_request
54
+ process_response(res)
55
+ end
56
+
57
+ def process_response(res)
58
+ xml = Peddler::Handlers::XMLHandler.decode_response(res)
59
+ params = Peddler::Handlers::XMLHandler.parse(:report, xml)
60
+ if params[0]
61
+ params[0].each_pair do |key, value|
62
+ if key == "ListOfDownloads"
63
+ params = Peddler::Handlers::XMLHandler.parse(:download, value)
64
+ @download = Peddler::Feeds::Download.new(@transport, params[0])
65
+ @batch = Peddler::Handlers::TabDelimitedHandler.decode_response(@download.to_s)
66
+ else
67
+ self.send "#{@mapped_params[key]}=", value
68
+ end
69
+ end
70
+ end
71
+ end
72
+ end
73
+
74
+ # This is an unshipped order.
75
+ class Item
76
+ attr_accessor :order_id, :order_item_id, :quantity, :ship_date, :carrier_name, :tracking_number, :ship_method
77
+ attr_reader :carrier_code
78
+
79
+ def initialize(params={})
80
+ params.each_pair{ |key, value| send("#{key}=", value) }
81
+ end
82
+
83
+ # Validates when setting carrier code.
84
+ def carrier_code=(carrier_code)
85
+ @carrier_code = carrier_code if %w{USPS UPS FedEx other}.include?(carrier_code)
86
+ end
87
+
88
+ # Outputs a formatted line for the tab-delimited upload file.
89
+ def to_s
90
+ "#{@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"
91
+ end
92
+ end
93
+ end
94
+ end
@@ -0,0 +1,135 @@
1
+ module Peddler
2
+ class PeddlerError < StandardError
3
+ def initialize(msg)
4
+ super("#{msg}")
5
+ end
6
+ end
7
+
8
+ # Our work horse. Runs on top of Net::HTTP.
9
+ class Transport
10
+ API_HOSTS = {:us => "secure.amazon.com",
11
+ :uk => "secure.amazon.co.uk",
12
+ :de => "secure.amazon.de",
13
+ :ca => "secure.amazon.ca",
14
+ :fr => "secure.amazon.fr",
15
+ :jp => "vendornet.amazon.co.jp" }
16
+ BASE_HEADERS = {"User-Agent" => "Peddler/#{Peddler::VERSION}",
17
+ "Content-Type" => "text/xml;charset=utf-8",
18
+ "Cookie" => "x-main=YvjPkwfntqDKun0QEmVRPcTTZDMe?Tn?; ubid-main=002-8989859-9917520; ubid-tacbus=019-5423258-4241018;x-tacbus=vtm4d53DvX@Sc9LxTnAnxsFL3DorwxJa; ubid-tcmacb=087-8055947-0795529; ubid-ty2kacbus=161-5477122-2773524; session-id=087-178254-5924832;session-id-time=950660664"}
19
+
20
+ BASE_PARAMS = {
21
+ "Service" => "MerchantQueryService"
22
+ }
23
+
24
+ attr_writer :username, :password
25
+ attr_accessor :path, :query_params, :headers, :body
26
+
27
+ #Returns request instance
28
+ def request
29
+ req = request_method.new("#{self.path.gsub(/\/$/, "")}/#{self.query_string}")
30
+ self.headers.each do |header, value|
31
+ if header.kind_of? Symbol
32
+ req[header.to_s.gsub(/_/, "")] = value
33
+ else
34
+ req[header] = value
35
+ end
36
+ end
37
+ req.basic_auth(@username, @password) if @username && @password
38
+ req.body = self.body unless self.body.empty?
39
+ req
40
+ end
41
+
42
+ def execute_request
43
+ begin
44
+ self.conn.start do |http|
45
+ res = http.request(self.request)
46
+ case res
47
+ when Net::HTTPSuccess
48
+ res.body
49
+ else
50
+ raise PeddlerError.new(res.body)
51
+ end
52
+ end
53
+ end
54
+ end
55
+
56
+ def clear_request
57
+ self.headers = BASE_HEADERS.dup
58
+ self.body = ""
59
+ end
60
+
61
+ def legacize_request
62
+ self.clear_request
63
+ self.path = "/exec/panama/seller-admin/"
64
+ self.query_params = {}
65
+ end
66
+
67
+ def modernize_request
68
+ self.clear_request
69
+ self.path = "/query/"
70
+ self.query_params = BASE_PARAMS.dup
71
+ end
72
+
73
+ def region=(region)
74
+ @conn = nil
75
+ region = region.to_sym
76
+ if API_HOSTS.has_key?(region)
77
+ @region = region
78
+ else
79
+ raise PeddlerError.new("Region not recognized")
80
+ end
81
+ end
82
+
83
+ def url
84
+ URI.parse("https://#{self.host}#{self.path.gsub(/\/$/, "")}/#{self.query_string}")
85
+ end
86
+
87
+ def dump_headers(msg)
88
+ msg.each_header do |key, value|
89
+ p "#{key}=#{value}"
90
+ end
91
+ end
92
+
93
+ protected
94
+ #Returns the Net::HTTP instance.
95
+ def conn
96
+ if @conn
97
+ @conn
98
+ else
99
+ conn = Net::HTTP.new(host, 443)
100
+ conn.use_ssl = true
101
+ conn.verify_mode = OpenSSL::SSL::VERIFY_NONE
102
+ @conn = conn
103
+ end
104
+ end
105
+
106
+ def request_method
107
+ if !self.body.empty? || !self.query_params.empty?
108
+ Net::HTTP::Post
109
+ else
110
+ Net::HTTP::Get
111
+ end
112
+ end
113
+
114
+ def host
115
+ API_HOSTS[@region]
116
+ end
117
+
118
+ def query_string
119
+ unless query_params.empty?
120
+ query_params.inject("?") do |out, pair|
121
+ key, value = pair
122
+ key = key.to_s.gsub(/_([a-z])/) { $1.upcase } if key.kind_of? Symbol
123
+ value = value.httpdate if value.respond_to? :httpdate
124
+ out += "&" if out.size > 1
125
+ "#{out}#{url_encode(key)}=#{url_encode(value)}"
126
+ end
127
+ end
128
+ end
129
+
130
+ def url_encode(value)
131
+ require 'cgi' unless defined?(CGI) && defined?(CGI::escape)
132
+ CGI.escape(value.to_s)
133
+ end
134
+ end
135
+ end