ramazon_advertising 0.3.2

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.
Files changed (43) hide show
  1. data/.document +5 -0
  2. data/.gitignore +7 -0
  3. data/LICENSE +20 -0
  4. data/README.rdoc +46 -0
  5. data/Rakefile +84 -0
  6. data/VERSION +1 -0
  7. data/features/friendly_errors.feature +15 -0
  8. data/features/generate_root_browse_nodes.feature +13 -0
  9. data/features/getting_offer_details.feature +30 -0
  10. data/features/getting_search_bins.feature +22 -0
  11. data/features/retrieve_browse_node_information.feature +16 -0
  12. data/features/retrieving_a_product.feature +32 -0
  13. data/features/searching_for_products.feature +47 -0
  14. data/features/step_definitions/auth_steps.rb +8 -0
  15. data/features/step_definitions/browse_node_steps.rb +55 -0
  16. data/features/step_definitions/error_steps.rb +14 -0
  17. data/features/step_definitions/product_collection_steps.rb +20 -0
  18. data/features/step_definitions/product_steps.rb +75 -0
  19. data/features/step_definitions/ramazon_advertising_steps.rb +0 -0
  20. data/features/support/env.rb +7 -0
  21. data/features/support/ramazon_advertising.example.yml +2 -0
  22. data/lib/ramazon/abstract_element.rb +18 -0
  23. data/lib/ramazon/browse_node.rb +69 -0
  24. data/lib/ramazon/configuration.rb +66 -0
  25. data/lib/ramazon/error.rb +12 -0
  26. data/lib/ramazon/image.rb +18 -0
  27. data/lib/ramazon/merchant.rb +21 -0
  28. data/lib/ramazon/offer.rb +27 -0
  29. data/lib/ramazon/price.rb +18 -0
  30. data/lib/ramazon/product.rb +303 -0
  31. data/lib/ramazon/product_collection.rb +30 -0
  32. data/lib/ramazon/rails_additions.rb +99 -0
  33. data/lib/ramazon/request.rb +82 -0
  34. data/lib/ramazon/search_bin.rb +11 -0
  35. data/lib/ramazon/search_bin_parameter.rb +9 -0
  36. data/lib/ramazon/search_bin_set.rb +11 -0
  37. data/lib/ramazon/signatory.rb +10 -0
  38. data/lib/ramazon_advertising.rb +41 -0
  39. data/lib/root_nodes.yml +64 -0
  40. data/lib/tasks/ramazon.rake +9 -0
  41. data/spec/ramazon/configuration_spec.rb +21 -0
  42. data/spec/spec_helper.rb +11 -0
  43. metadata +157 -0
@@ -0,0 +1,75 @@
1
+ When /^I try to find the asin "([^\"]*)"$/ do |asin|
2
+ @products = Ramazon::Product.find(:item_id => asin, :response_group => ["Medium", "BrowseNodes", "Offers"])
3
+ @product = @products[0]
4
+ end
5
+
6
+ Given /^I am searching with the "([^\"]*)" of "([^\"]*)"$/ do |attr, value|
7
+ @search_options ||= {}
8
+ @search_options[attr.to_sym] = value
9
+ end
10
+
11
+ When /^I perform the product search$/ do
12
+ begin
13
+ @search_options ||= {}
14
+ @products = Ramazon::Product.find(@search_options)
15
+ rescue Ramazon::Error => e
16
+ @error = e
17
+ end
18
+ end
19
+
20
+ Then /^I should get a list of products$/ do
21
+ raise @error if @error
22
+ @products.should_not be_empty
23
+ end
24
+
25
+ Then /^the list of products should have more than (\d+) product$/ do |count|
26
+ @products.should have_at_least(count.to_i + 1).items
27
+ end
28
+
29
+
30
+ Then /^each product should have the "([^\"]*)" "([^\"]*)"$/ do |attr, value|
31
+ @products.each do |p|
32
+ p.send(attr).should eql(value)
33
+ end
34
+ end
35
+
36
+ Then /^I should get a product$/ do
37
+ raise @error if @error
38
+ @product = @products[0] unless @product
39
+ @product.should_not be_nil
40
+ end
41
+
42
+ Then /^the product should have (the\s)?"([^\"]*)" "([^\"]*)"$/ do |the, attr, value|
43
+ @product.send(attr).should eql(value)
44
+ end
45
+
46
+ Then /^the product should have (a\s)?"([^\"]*)"$/ do |a, attr|
47
+ @product.send(attr).should_not be_nil
48
+ end
49
+
50
+ Then /^each of the product's "([^\"]*)" should have a "([^\"]*)"$/ do |collection_name, attr|
51
+ @product.send(collection_name).should_not be_empty
52
+ @product.send(collection_name).each do |i|
53
+ i.send(attr).should_not be_nil
54
+ end
55
+ end
56
+
57
+ Then /^each of the product's "([^\"]*)" should have a "([^\"]*)" of "([^\"]*)"$/ do |collection_name, attr, value|
58
+ @product.send(collection_name).should_not be_empty
59
+ @product.send(collection_name).each do |i|
60
+ i.send(attr).should_not be_nil
61
+ end
62
+ end
63
+
64
+
65
+ Then /^the product should have a category tree for "([^\"]*)"$/ do |category_name|
66
+ @product.category_tree[category_name].should_not be_nil
67
+ end
68
+
69
+
70
+ Then /^each product should have (a\s)?"([^\"]*)"$/ do |a, attr|
71
+ @products.each do |p|
72
+ p.send(attr).should_not be_nil
73
+ end
74
+ end
75
+
@@ -0,0 +1,7 @@
1
+ $LOAD_PATH.unshift(File.join(File.dirname(__FILE__) + '..', '..', 'lib'))
2
+ require 'lib/ramazon_advertising'
3
+
4
+ require 'spec/expectations'
5
+
6
+ configatron.ramazon.configure_from_yaml(File.join(File.dirname(__FILE__), "ramazon_advertising.yml"))
7
+
@@ -0,0 +1,2 @@
1
+ access_key: SOMETHING
2
+ secret_key: SOMETHING SECRET
@@ -0,0 +1,18 @@
1
+ module Ramazon
2
+ module AbstractElement
3
+ def self.included(base)
4
+ base.extend(ClassMethods)
5
+ end
6
+
7
+ module ClassMethods
8
+ def abstract_element(name, type, options = {})
9
+ element(name, type, options.merge(:parser => :abstract_parse, :raw => true))
10
+ end
11
+
12
+ def abstract_parse(xml, options = {})
13
+ tag XML::Parser.string(xml).parse.root.name
14
+ parse(xml)
15
+ end
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,69 @@
1
+ module Ramazon
2
+ class BrowseNode
3
+ include HTTParty
4
+ include HappyMapper
5
+
6
+ tag "BrowseNode"
7
+
8
+ element :name, String, :tag => "Name"
9
+ element :node_id, String, :tag => "BrowseNodeId"
10
+ element :children, Ramazon::BrowseNode, :tag => "Children/BrowseNode"
11
+ element :is_category_root, Boolean, :tag => "IsCategoryRoot"
12
+
13
+ attr_accessor :child
14
+
15
+ DEFAULT_ROOT_FILE = File.join(File.dirname(__FILE__), '..', 'root_nodes.yml')
16
+
17
+ def self.generate_root_nodes(file_name = DEFAULT_ROOT_FILE)
18
+ if Ramazon::Configuration.locale == :us
19
+ doc = Nokogiri::HTML(get('http://www.amazon.com').body)
20
+ root_nodes = {}
21
+ doc.search(".navSaMenu .navSaChildItem a").each do |element|
22
+ if element["href"] =~ /node=(\d+)\&/
23
+ root_nodes[element.content] = $1
24
+ end
25
+ end
26
+
27
+ unless root_nodes.empty?
28
+ FileUtils.rm_f(file_name)
29
+ File.open(file_name, 'w') do |f|
30
+ f.write(root_nodes.to_yaml)
31
+ end
32
+ end
33
+ else
34
+ #todo correlate ECS locales to actual amazon.* urls
35
+ raise "generating root nodes for locale's other than the US is not supported"
36
+ end
37
+ end
38
+
39
+ def self.root_nodes(file_name = DEFAULT_ROOT_FILE)
40
+ @root_nodes ||= File.open(file_name) { |yf| YAML::load(yf) }
41
+ end
42
+
43
+ # find a browse node based on its id
44
+ # @param node_id [String] the browse node you're looking for
45
+ # @returns [Ramazon::BrowseNode] the node you're looking for
46
+ def self.find(node_id)
47
+ req = Ramazon::Request.new(:operation => "BrowseNodeLookup", :browse_node_id => node_id)
48
+ res = req.submit
49
+ parse(res.to_s)[0]
50
+ end
51
+
52
+ # get a hash of name -> child browse_nodes
53
+ # @returns [Hash] stringified hash of names => Ramazon::BrowseNode objects
54
+ def child_hash
55
+ if !@child_hash
56
+ @child_hash = {}
57
+ self.children.each do |i|
58
+ @child_hash[i.name] = i
59
+ end
60
+ end
61
+
62
+ @child_hash
63
+ end
64
+
65
+ def self.parse(xml, options = {})
66
+ super
67
+ end
68
+ end
69
+ end
@@ -0,0 +1,66 @@
1
+ module Ramazon
2
+ # the class that persists configuration information for Amazon requests
3
+ class Configuration
4
+ class << self
5
+ LOCALES = [:ca, :de, :fr, :jp, :uk, :us]
6
+
7
+ # set the locale for future requests
8
+ # will raise an exception if the locale isn't in the Configuration::LOCALES collection
9
+ def locale=(locale)
10
+ if LOCALES.include?(locale)
11
+ configatron.ramazon.locale = locale
12
+ else
13
+ raise "unknown locale"
14
+ end
15
+ end
16
+
17
+ # get the current locale (defaults to the us)
18
+ def locale
19
+ configatron.ramazon.locale || :us
20
+ end
21
+
22
+ # get the current access key
23
+ def access_key
24
+ configatron.ramazon.access_key
25
+ end
26
+
27
+ # set the current access key
28
+ # @param key [String] access key you're using to access the advertising api
29
+ def access_key=(key)
30
+ configatron.ramazon.access_key = key
31
+ end
32
+
33
+ # get the current secret key that is used for request signing
34
+ def secret_key
35
+ configatron.ramazon.secret_key
36
+ end
37
+
38
+ # set the secret key so that requests can be appropriately signed
39
+ # @param key [String] secret key you're using to sign advertising requests
40
+ def secret_key=(key)
41
+ configatron.ramazon.secret_key = key
42
+ end
43
+
44
+ # get the correct host based on your locale
45
+ def base_uri
46
+ if locale == :us
47
+ "http://ecs.amazonaws.com"
48
+ else
49
+ "http://ecs.amazonaws.#{locale}"
50
+ end
51
+ end
52
+
53
+ # get the full path including locale specific host and /onca/xml path
54
+ def uri
55
+ "#{base_uri}#{path}"
56
+ end
57
+
58
+ # get the path where requests should be dispatched to
59
+ def path
60
+ "/onca/xml"
61
+ end
62
+
63
+
64
+ end
65
+ end
66
+ end
@@ -0,0 +1,12 @@
1
+ module Ramazon
2
+ class Error < StandardError
3
+ include HappyMapper
4
+ tag "Error"
5
+ element :code, String, :tag => "Code"
6
+ element :message, String, :tag => "Message"
7
+
8
+ def to_s
9
+ "#{self.code}: #{self.message}"
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,18 @@
1
+ module Ramazon
2
+ # An image object returned by Amazon's product data
3
+ # Available accessors include
4
+ # +url+::
5
+ # The url of the image
6
+ # +height+::
7
+ # The height of the image
8
+ # +width+::
9
+ # The width of the image
10
+ class Image
11
+ include HappyMapper
12
+ include Ramazon::AbstractElement
13
+ tag "Image"
14
+ element :url, String, :tag => "URL"
15
+ element :height, Integer, :tag => "Height"
16
+ element :width, Integer, :tag => "Width"
17
+ end
18
+ end
@@ -0,0 +1,21 @@
1
+ module Ramazon
2
+ # Merchant Details
3
+ # Current supports the following accessors
4
+ # +merchant_id+::
5
+ # The id of the merchant
6
+ # +glance_page_url+::
7
+ # The url of the merchant
8
+ # +average_feedback_rating+::
9
+ # Average Feedback Rating
10
+ # +total_feedback+::
11
+ # Total Feedback
12
+ class Merchant
13
+ include HappyMapper
14
+ tag "Merchant"
15
+
16
+ element :merchant_id, String, :tag => "MerchantId"
17
+ element :glance_page_url, String, :tag => "GlancePage"
18
+ element :average_feedback_rating, Float, :tag => "AverageFeedbackRating"
19
+ element :total_feedback, Integer, :tag => "TotalFeedback"
20
+ end
21
+ end
@@ -0,0 +1,27 @@
1
+ module Ramazon
2
+ # Offer Details
3
+ # Currently supports the following accessors
4
+ # +merchant+::
5
+ # The merchant that is listing this product (NOTE: Returns Ramazon::Merchant object)
6
+ # +condition+::
7
+ # The condition of the product
8
+ # +sub_condition+::
9
+ # The subcondition of the product
10
+ # +listing_id+::
11
+ # The id of the listing
12
+ # +price+::
13
+ # The asking price of the listing (NOTE: Returns Ramazon::Price object)
14
+ class Offer
15
+ include HappyMapper
16
+ include Ramazon::AbstractElement
17
+
18
+ tag "Offer"
19
+
20
+ has_one :merchant, Ramazon::Merchant, :tag => "Merchant"
21
+ element :condition, String, :tag => "OfferAttributes/Condition"
22
+ element :sub_condition, String, :tag => "OfferAttributes/SubCondition"
23
+ element :listing_id, String, :tag => "OfferListing/OfferListingId"
24
+ abstract_element :price, Ramazon::Price, :tag => "OfferListing/Price"
25
+
26
+ end
27
+ end
@@ -0,0 +1,18 @@
1
+ module Ramazon
2
+ # A price object returned by Amazon's product data
3
+ # Available accessors include
4
+ # +amount+::
5
+ # The unformatted ammount of the price
6
+ # +currency_code+::
7
+ # The currency of the offer
8
+ # +formatted_price+::
9
+ # The formatted price of the offer
10
+ class Price
11
+ include HappyMapper
12
+ include Ramazon::AbstractElement
13
+
14
+ element :amount, String, :tag => "Amount"
15
+ element :currency_code, String, :tag => "CurrencyCode"
16
+ element :formatted_price, String, :tag => "FormattedPrice"
17
+ end
18
+ end
@@ -0,0 +1,303 @@
1
+ module Ramazon
2
+ # Find and get product details with this class
3
+ # Currently supports the following accessors
4
+ # (all other elements can be accessed via nokogiri selectors and the get method)
5
+ # +asin+::
6
+ # Amazon Identifier
7
+ # +upc+::
8
+ # UPC ID
9
+ # +title+::
10
+ # Title of the product
11
+ # +product_group+::
12
+ # The category/product_group of the product
13
+ # +manufacturer+::
14
+ # The manufacturer of the product
15
+ # +brand+::
16
+ # The brand of the product
17
+ # +url+::
18
+ # The Amazon URL of the product
19
+ # +small_image+::
20
+ # The small image that Amazon provides (NOTE: Returns Ramazon::Image object)
21
+ # +medium_image+::
22
+ # The medium image that Amazon provides (NOTE: Returns Ramazon::Image object)
23
+ # +large_image+::
24
+ # The large image that Amazon provides (NOTE: Returns Ramazon::Image object)
25
+ # +list_price+::
26
+ # The list price of the item (NOTE: Returns Ramazon::Price object)
27
+ # +lowest_new_price+::
28
+ # The lowest new price from the offer summary (NOTE: Returns Ramazon::Price object)
29
+ # +sales_rank+::
30
+ # The sales rank of the product
31
+ # +new_count+::
32
+ # The quantity of new item offers
33
+ # +used_count+::
34
+ # The quantity of used item offers
35
+ # +collectible_count+::
36
+ # The quantity of collectible item offers
37
+ # +refurbished_count+::
38
+ # The quantity of refurbished item offers
39
+ # +release_date+::
40
+ # The release date of the product
41
+ # +original_release_date+::
42
+ # The original release date of the product
43
+ # +offers+::
44
+ # The collection of offers available for the given product
45
+ # @example find an individual item
46
+ # @products = Ramazon::Product.find(:item_id => "B000NU2CY4", :response_group => "Medium")
47
+ # @products[0].title
48
+ # @products[0].asin
49
+ # @products[0].upc
50
+ # @products[0].large_image.url
51
+ # @products[0].url
52
+ #
53
+
54
+ class Product
55
+ include HappyMapper
56
+ include Ramazon::AbstractElement
57
+ tag 'Item'
58
+
59
+ element :asin, String, :tag => 'ASIN'
60
+ element :upc, String, :tag => 'ItemAttributes/UPC'
61
+ element :title, String, :tag => 'Title', :deep => true
62
+ element :product_group, String, :tag => 'ItemAttributes/ProductGroup'
63
+ element :manufacturer, String, :tag => 'ItemAttributes/Manufacturer'
64
+ element :brand, String, :tag => 'ItemAttributes/Brand'
65
+ element :url, String, :tag => 'DetailPageURL'
66
+ abstract_element :small_image, Ramazon::Image, :tag => "SmallImage"
67
+ abstract_element :large_image, Ramazon::Image, :tag => "LargeImage"
68
+ abstract_element :medium_image, Ramazon::Image, :tag => "MediumImage"
69
+ abstract_element :tiny_image, Ramazon::Image, :tag => "TinyImage"
70
+ abstract_element :thumb_image, Ramazon::Image, :tag => "ThumbImage"
71
+ abstract_element :list_price, Ramazon::Price, :tag => "ItemAttributes/ListPrice"
72
+ abstract_element :lowest_new_price, Ramazon::Price, :tag => "OfferSummary/LowestNewPrice"
73
+ has_many :offers, Ramazon::Offer, :tag => "Offers/Offer"
74
+
75
+ element :sales_rank, Integer, :tag => "SalesRank"
76
+
77
+ element :new_count, Integer, :tag => "OfferSummary/TotalNew"
78
+ element :used_count, Integer, :tag => "OfferSummary/TotalUsed"
79
+ element :collectible_count, Integer, :tag => "OfferSummary/TotalCollectible"
80
+ element :refurbished_count, Integer, :tag => "OfferSummary/TotalRefurbished"
81
+
82
+ element :release_date, Date, :tag => 'ItemAttributes/ReleaseDate'
83
+ element :original_release_date, Date, :tag => 'ItemAttributes/OriginalReleaseDate'
84
+
85
+ attr_accessor :has_first_page_of_full_offers
86
+
87
+ # Creates the worker that performs the delta indexing
88
+ # @param options Amazon request options (you can use an underscore convention)
89
+ # (ie. passing the :response_group option will be converted to "ResponseGroup")
90
+ # <tt>:item_id</tt> - the ASIN or UPC you're looking for
91
+ # @return [Array] array of Ramazon::Product objects
92
+ def self.find(*args)
93
+ options = args.extract_options!
94
+ if options[:item_id]
95
+ item_lookup(options[:item_id], options)
96
+ else
97
+ options[:operation] ||= "ItemSearch"
98
+ options[:search_index] ||= "Blended"
99
+ options[:item_page] ||= 1
100
+ res = Ramazon::Request.new(options).submit
101
+ products = Ramazon::ProductCollection.create_from_results(options[:item_page] || 1, 10, res)
102
+ if find_options_retrieve_all_offers?(options)
103
+ products.each do |p|
104
+ p.has_first_page_of_full_offers = true
105
+ p.offer_pages = offer_pages_for(p)
106
+ end
107
+ end
108
+ products
109
+ end
110
+ end
111
+
112
+ # Performs an item lookup
113
+ # @param item_id the ASIN or UPC you're looking for
114
+ # @options additional Amazon request options (i.e. :response_group)
115
+ def self.item_lookup(item_id, options = {})
116
+ req = Ramazon::Request.new({:item_id => item_id,
117
+ :operation => "ItemLookup"}.merge(options))
118
+ res = req.submit
119
+
120
+ Ramazon::ProductCollection.create_from_results(1,1,res)
121
+ end
122
+
123
+ # assembles the available images for the object
124
+ # @return [Hash] hash of symbolized image_name => Ramazon::Image pairs
125
+ def images
126
+ if !@images
127
+ @images = {}
128
+ @images[:thumb] = self.thumb_image if self.thumb_image
129
+ @images[:tiny_image] = self.tiny_image if self.tiny_image
130
+ @images[:small] = self.small_image if self.small_image
131
+ @images[:medium] = self.medium_image if self.medium_image
132
+ @images[:large] = self.large_image if self.large_image
133
+ end
134
+ end
135
+
136
+ attr_accessor :xml_doc
137
+ def self.parse(xml, options = {})
138
+ node = XML::Parser.string(xml.to_s).parse.root
139
+ node.find("//Item").collect do |n|
140
+ p = super(n.to_s)
141
+ p.xml_doc = Nokogiri::XML.parse(n.to_s)
142
+ p
143
+ end
144
+ end
145
+
146
+ # perform a nokogiri search on the product's XML
147
+ # @param args Passes directly to a Nokogiri::Xml.parse(xml).search method
148
+ # @example find the actor
149
+ # @product = Ramazon::Product.find(:item_id => "B000NU2CY4", :response_group => "Medium")[0]
150
+ # @product.get("ItemAttributes Actor").collect{|a| a.content}
151
+ def get(*args)
152
+ result = @xml_doc.search(args)
153
+ end
154
+
155
+ # returns a hash of category browse nodes from the top down
156
+ def category_tree
157
+ @category_tree = {}
158
+ get_category_browse_nodes.each do |n|
159
+ build_category_tree(n)
160
+ end
161
+ @category_tree
162
+ end
163
+
164
+ # a sorted list of used offers
165
+ def used_offers
166
+ if @used_offers.nil?
167
+ @used_offers = []
168
+ self.offers.each do |o|
169
+ if o.condition.downcase == "used"
170
+ @used_offers << o
171
+ end
172
+ end
173
+ @used_offers.sort!{|a,b| a.price.amount <=> b.price.amount}
174
+ end
175
+ @used_offers
176
+ end
177
+
178
+ # breaks down all the offers in a nested hash of [condition][subcondition]
179
+ # note: this will load ALL offers into memory
180
+ # @return [Hash] a nest hash of offers [condition][subcondition] => Array of Ramazon::Offer objects
181
+ def offers_by_condition
182
+ @offer_hash = {}
183
+ offer_page = 1
184
+
185
+ offers = offer_page(offer_page)
186
+ while offer_page <= offer_pages
187
+ offers.each do |o|
188
+ @offer_hash[o.condition.downcase] ||= {}
189
+ @offer_hash[o.condition.downcase][o.sub_condition] ||= []
190
+ @offer_hash[o.condition.downcase][o.sub_condition] << o
191
+ end
192
+
193
+ offer_page += 1
194
+ offers = offer_page(offer_page)
195
+ end
196
+
197
+ @offer_hash
198
+ end
199
+
200
+ # gets the number of offer pages for the specified product
201
+ # @param [Ramazon::Product] product the we want to get offer pages for
202
+ # @returns [Integer] number of offer pages
203
+ def self.offer_pages_for(product)
204
+ if !@offer_pages
205
+ offer_page_tags = product.get("//Offers/TotalOfferPages")
206
+ if offer_page_tags.size > 0
207
+ offer_pages = offer_page_tags[0].content.to_i
208
+ else
209
+ offer_pages = 1
210
+ end
211
+ end
212
+
213
+ offer_pages
214
+ end
215
+
216
+ # get the lowest offers broken down by subcondition
217
+ # @return [Hash] a nested hash of prices ie ["new"]["mint"] => Ramazon::Offer
218
+ def lowest_offers
219
+ if @lowest_offers.nil?
220
+ @lowest_offers = {}
221
+ offers_by_condition.each do |condition, sub_conditions|
222
+ @lowest_offers[condition] = {}
223
+ sub_conditions.each do |sub_condition, col|
224
+ sorted_offers = col.sort{|a,b| a.price.amount.to_i <=> b.price.amount.to_i}
225
+ @lowest_offers[condition][sub_condition] = sorted_offers.first
226
+ end
227
+ end
228
+ end
229
+
230
+ @lowest_offers
231
+ end
232
+
233
+ def offer_pages=(pages)
234
+ @offer_pages = pages.to_i
235
+ end
236
+
237
+ def offer_pages
238
+ @offer_pages
239
+ end
240
+
241
+ #get offers from a given page
242
+ # @param page [Integer] the page number you want to get
243
+ # @return [Array] Array of Offers returned from the page
244
+ def offer_page(page = 1)
245
+ #get all offers
246
+ if page == 1 && has_first_page_of_full_offers
247
+ self.offers
248
+ else
249
+ products = self.class.find(:item_id => self.asin,
250
+ :response_group => "OfferListings",
251
+ :merchant_id => "All",
252
+ :condition => "All",
253
+ :offer_page => page)
254
+
255
+ if products
256
+ product = products[0]
257
+ self.offer_pages = self.class.offer_pages_for(product)
258
+ product.offers
259
+ else
260
+ []
261
+ end
262
+ end
263
+ end
264
+
265
+ private
266
+ # recursive function used to generate a topdown category tree
267
+ def build_category_tree(n, child = nil)
268
+ amz_node = BrowseNode.parse(n.to_s)
269
+ amz_node.child = child unless child.nil?
270
+
271
+ if n.search("./IsCategoryRoot").size > 0
272
+ @category_tree[amz_node.name] ||= []
273
+ @category_tree[amz_node.name] << amz_node
274
+ else
275
+ parents = n.search("./Ancestors/BrowseNode")
276
+ if parents.size > 0
277
+ build_category_tree(parents[0], amz_node)
278
+ end
279
+ end
280
+
281
+
282
+ end
283
+
284
+ def get_category_browse_nodes
285
+ self.get("BrowseNodes BrowseNode IsCategoryRoot").collect do |n|
286
+ n.ancestors("//BrowseNodes/BrowseNode")
287
+ end
288
+ end
289
+
290
+ def self.find_options_retrieve_all_offers?(options = {})
291
+ if options[:response_group].is_a?(Array)
292
+ groups = options[:response_group].join(" ")
293
+ else
294
+ groups = options[:response_group] || ""
295
+ end
296
+
297
+ groups =~ /OfferListings/ &&
298
+ (options[:offer_page].nil? || options[:offer_page].to_i == 1) &&
299
+ (options[:merchant_id] == "All") &&
300
+ (options[:condition] == "All")
301
+ end
302
+ end
303
+ end