shopsavvy-sdk 1.0.0 ā 1.0.1
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.
- checksums.yaml +4 -4
- data/README.md +656 -270
- data/lib/shopsavvy_data_api/version.rb +1 -1
- metadata +2 -5
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 6179b544c407fb648b93d79b8a0b62e2aacb3bbfbebd7f3b741887065f06cd03
|
4
|
+
data.tar.gz: 8802bfeb393b61ad00d75e139649150cf1b7ed9be50b36b443dd3951c97fd11d
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 92ebba528d0ba098c7fa731b8fa854cb9945503eb2e9f096d5258c4be33de8dee334d52be21d04a4ba84a06ecf144e6acc05557323a49a91dcff087f7ab3b8aa
|
7
|
+
data.tar.gz: 7c6104eb59e8112921087076bf4794d63e8fc3d0d7ccb29be61c26e53bd2be20d4735aa461693f31fb4016f57445859a35a9e2cf3cbe5c9683276dabdab5439c
|
data/README.md
CHANGED
@@ -1,172 +1,316 @@
|
|
1
1
|
# ShopSavvy Data API - Ruby SDK
|
2
2
|
|
3
|
-
[](https://badge.fury.io/rb/shopsavvy-sdk)
|
4
|
+
[](https://www.ruby-lang.org/)
|
5
5
|
[](https://opensource.org/licenses/MIT)
|
6
|
+
[](https://shopsavvy.com/data/documentation)
|
6
7
|
|
7
|
-
Official Ruby SDK for the [ShopSavvy Data API](https://shopsavvy.com/data). Access product data, pricing
|
8
|
+
Official Ruby SDK for the [ShopSavvy Data API](https://shopsavvy.com/data). Access comprehensive product data, real-time pricing, and historical price trends across **thousands of retailers** and **millions of products**.
|
8
9
|
|
9
|
-
##
|
10
|
-
|
11
|
-
### Installation
|
12
|
-
|
13
|
-
Add this line to your application's Gemfile:
|
10
|
+
## ā” 30-Second Quick Start
|
14
11
|
|
15
12
|
```ruby
|
16
|
-
|
13
|
+
# Install
|
14
|
+
gem install shopsavvy-sdk
|
15
|
+
|
16
|
+
# Use
|
17
|
+
require 'shopsavvy_data_api'
|
18
|
+
client = ShopsavvyDataApi.new(api_key: 'ss_live_your_api_key_here')
|
19
|
+
product = client.get_product_details('012345678901')
|
20
|
+
puts "#{product.data.name} - Best price: $#{client.get_current_offers('012345678901').data.min_by(&:price).price}"
|
17
21
|
```
|
18
22
|
|
19
|
-
|
23
|
+
## š Feature Comparison
|
20
24
|
|
21
|
-
|
22
|
-
|
23
|
-
|
25
|
+
| Feature | Free Tier | Pro | Enterprise |
|
26
|
+
|---------|-----------|-----|-----------|
|
27
|
+
| **API Calls/Month** | 1,000 | 100,000 | Unlimited |
|
28
|
+
| **Product Details** | ā
| ā
| ā
|
|
29
|
+
| **Real-time Pricing** | ā
| ā
| ā
|
|
30
|
+
| **Price History** | 30 days | 1 year | 5+ years |
|
31
|
+
| **Bulk Operations** | 10/batch | 100/batch | 1000/batch |
|
32
|
+
| **Retailer Coverage** | 50+ | 500+ | 1000+ |
|
33
|
+
| **Rate Limiting** | 60/hour | 1000/hour | Custom |
|
34
|
+
| **Support** | Community | Email | Phone + Dedicated |
|
24
35
|
|
25
|
-
|
36
|
+
## š Installation & Setup
|
26
37
|
|
38
|
+
### Installation
|
39
|
+
|
40
|
+
Add to your Gemfile:
|
41
|
+
```ruby
|
42
|
+
gem 'shopsavvy-sdk'
|
43
|
+
```
|
44
|
+
|
45
|
+
Or install directly:
|
27
46
|
```bash
|
28
|
-
gem install
|
47
|
+
gem install shopsavvy-sdk
|
29
48
|
```
|
30
49
|
|
31
50
|
### Get Your API Key
|
32
51
|
|
33
|
-
1. Visit [shopsavvy.com/data](https://shopsavvy.com/data)
|
34
|
-
2.
|
35
|
-
3.
|
36
|
-
4.
|
52
|
+
1. **Sign up**: Visit [shopsavvy.com/data](https://shopsavvy.com/data)
|
53
|
+
2. **Choose plan**: Select based on your usage needs
|
54
|
+
3. **Get API key**: Copy from your dashboard
|
55
|
+
4. **Test**: Run the 30-second example above
|
37
56
|
|
38
|
-
###
|
57
|
+
### Environment Setup
|
39
58
|
|
40
59
|
```ruby
|
41
|
-
|
42
|
-
|
43
|
-
# Initialize the client
|
44
|
-
client = ShopsavvyDataApi.new(api_key: "ss_live_your_api_key_here")
|
60
|
+
# For production, use environment variables
|
61
|
+
ENV['SHOPSAVVY_API_KEY'] = 'ss_live_your_api_key_here'
|
45
62
|
|
46
|
-
#
|
47
|
-
|
48
|
-
puts product.data.name
|
49
|
-
|
50
|
-
# Get current prices from all retailers
|
51
|
-
offers = client.get_current_offers("012345678901")
|
52
|
-
offers.data.each do |offer|
|
53
|
-
puts "#{offer.retailer}: $#{offer.price}"
|
54
|
-
end
|
55
|
-
|
56
|
-
# Get price history
|
57
|
-
history = client.get_price_history(
|
58
|
-
"012345678901",
|
59
|
-
"2024-01-01",
|
60
|
-
"2024-01-31"
|
61
|
-
)
|
63
|
+
# Initialize client
|
64
|
+
client = ShopsavvyDataApi.new(api_key: ENV['SHOPSAVVY_API_KEY'])
|
62
65
|
```
|
63
66
|
|
64
|
-
## š API Reference
|
67
|
+
## š Complete API Reference
|
65
68
|
|
66
69
|
### Client Configuration
|
67
70
|
|
68
71
|
```ruby
|
69
|
-
#
|
72
|
+
# Basic configuration
|
73
|
+
client = ShopsavvyDataApi.new(
|
74
|
+
api_key: 'ss_live_your_api_key_here',
|
75
|
+
timeout: 30, # Request timeout in seconds
|
76
|
+
base_url: 'https://api.shopsavvy.com/v1' # Custom base URL
|
77
|
+
)
|
78
|
+
|
79
|
+
# Advanced configuration with retry logic
|
70
80
|
client = ShopsavvyDataApi.new(
|
71
|
-
api_key:
|
72
|
-
timeout:
|
73
|
-
|
81
|
+
api_key: 'ss_live_your_api_key_here',
|
82
|
+
timeout: 60,
|
83
|
+
retry_attempts: 3,
|
84
|
+
retry_delay: 1.0,
|
85
|
+
user_agent: 'MyApp/1.0.0'
|
74
86
|
)
|
75
87
|
|
76
|
-
#
|
88
|
+
# Configuration object approach
|
77
89
|
config = ShopsavvyDataApi::Configuration.new(
|
78
|
-
api_key:
|
79
|
-
timeout:
|
90
|
+
api_key: 'ss_live_your_api_key_here',
|
91
|
+
timeout: 120,
|
92
|
+
debug: true # Enable debug logging
|
80
93
|
)
|
81
94
|
client = ShopsavvyDataApi.with_config(config)
|
82
|
-
|
83
|
-
# Method 3: Direct client instantiation
|
84
|
-
client = ShopsavvyDataApi::Client.new(api_key: "ss_live_your_api_key_here")
|
85
95
|
```
|
86
96
|
|
87
97
|
### Product Lookup
|
88
98
|
|
89
99
|
#### Single Product
|
90
100
|
```ruby
|
91
|
-
# Look up by barcode, ASIN, URL, or
|
101
|
+
# Look up by barcode, ASIN, URL, model number, or ShopSavvy ID
|
92
102
|
product = client.get_product_details("012345678901")
|
93
103
|
amazon_product = client.get_product_details("B08N5WRWNW")
|
94
104
|
url_product = client.get_product_details("https://www.amazon.com/dp/B08N5WRWNW")
|
105
|
+
model_product = client.get_product_details("MQ023LL/A") # iPhone model number
|
95
106
|
|
96
107
|
puts "Product: #{product.data.name}"
|
97
108
|
puts "Brand: #{product.data.brand}"
|
98
109
|
puts "Category: #{product.data.category}"
|
110
|
+
puts "ASIN: #{product.data.asin}" if product.data.asin
|
111
|
+
puts "Model: #{product.data.model}" if product.data.model
|
112
|
+
puts "Description: #{product.data.description}" if product.data.description
|
99
113
|
```
|
100
114
|
|
101
|
-
####
|
115
|
+
#### Bulk Product Lookup
|
102
116
|
```ruby
|
103
|
-
products
|
104
|
-
|
105
|
-
"B08N5WRWNW",
|
106
|
-
"https://www.bestbuy.com/site/product/123456"
|
107
|
-
|
117
|
+
# Process up to 100 products at once (Pro plan)
|
118
|
+
identifiers = [
|
119
|
+
"012345678901", "B08N5WRWNW", "045496590048",
|
120
|
+
"https://www.bestbuy.com/site/product/123456",
|
121
|
+
"MQ023LL/A", "SM-S911U" # iPhone and Samsung model numbers
|
122
|
+
]
|
123
|
+
|
124
|
+
products = client.get_product_details_batch(identifiers)
|
108
125
|
|
109
126
|
products.data.each do |product|
|
110
|
-
puts "#{product.name} by #{product.brand}"
|
127
|
+
puts "#{product.name} by #{product.brand} - #{product.category}"
|
128
|
+
puts " Identifiers: #{product.identifiers}" if product.identifiers
|
129
|
+
end
|
130
|
+
|
131
|
+
# Handle potential errors in batch processing
|
132
|
+
products.data.each_with_index do |product, index|
|
133
|
+
if product.nil?
|
134
|
+
puts "Failed to find product: #{identifiers[index]}"
|
135
|
+
else
|
136
|
+
puts "ā Found: #{product.name}"
|
137
|
+
end
|
111
138
|
end
|
112
139
|
```
|
113
140
|
|
114
|
-
###
|
141
|
+
### Real-Time Pricing
|
115
142
|
|
116
|
-
#### All Retailers
|
143
|
+
#### All Retailers Analysis
|
117
144
|
```ruby
|
118
145
|
offers = client.get_current_offers("012345678901")
|
119
|
-
puts "Found #{offers.data.length} offers"
|
146
|
+
puts "Found #{offers.data.length} offers across retailers"
|
120
147
|
|
121
|
-
#
|
148
|
+
# Advanced price analysis
|
122
149
|
sorted_offers = offers.data.sort_by(&:price)
|
123
150
|
cheapest = sorted_offers.first
|
124
|
-
|
151
|
+
most_expensive = sorted_offers.last
|
152
|
+
|
153
|
+
puts "š° Best price: #{cheapest.retailer} - $#{cheapest.price}"
|
154
|
+
puts "šø Highest price: #{most_expensive.retailer} - $#{most_expensive.price}"
|
155
|
+
puts "š Average price: $#{offers.data.map(&:price).sum / offers.data.length}"
|
156
|
+
puts "š” Potential savings: $#{most_expensive.price - cheapest.price}"
|
157
|
+
|
158
|
+
# Filter by availability and condition
|
159
|
+
in_stock_offers = offers.data.select { |offer| offer.availability == 'in_stock' }
|
160
|
+
new_condition_offers = offers.data.select { |offer| offer.condition == 'new' }
|
161
|
+
|
162
|
+
puts "ā
In-stock offers: #{in_stock_offers.length}"
|
163
|
+
puts "š New condition: #{new_condition_offers.length}"
|
125
164
|
```
|
126
165
|
|
127
|
-
#### Specific
|
166
|
+
#### Retailer-Specific Queries
|
128
167
|
```ruby
|
168
|
+
# Major retailers
|
129
169
|
amazon_offers = client.get_current_offers("012345678901", retailer: "amazon")
|
170
|
+
walmart_offers = client.get_current_offers("012345678901", retailer: "walmart")
|
130
171
|
target_offers = client.get_current_offers("012345678901", retailer: "target")
|
172
|
+
bestbuy_offers = client.get_current_offers("012345678901", retailer: "bestbuy")
|
173
|
+
|
174
|
+
# Compare specific retailers
|
175
|
+
retailers = %w[amazon walmart target bestbuy]
|
176
|
+
retailer_prices = {}
|
177
|
+
|
178
|
+
retailers.each do |retailer|
|
179
|
+
offers = client.get_current_offers("012345678901", retailer: retailer)
|
180
|
+
if offers.data.any?
|
181
|
+
best_offer = offers.data.min_by(&:price)
|
182
|
+
retailer_prices[retailer] = best_offer.price
|
183
|
+
end
|
184
|
+
end
|
185
|
+
|
186
|
+
puts "Retailer price comparison:"
|
187
|
+
retailer_prices.sort_by { |_, price| price }.each do |retailer, price|
|
188
|
+
puts " #{retailer.capitalize}: $#{price}"
|
189
|
+
end
|
131
190
|
```
|
132
191
|
|
133
|
-
####
|
192
|
+
#### Bulk Price Monitoring
|
134
193
|
```ruby
|
135
|
-
|
136
|
-
|
137
|
-
"B08N5WRWNW"
|
138
|
-
|
194
|
+
# Monitor multiple products simultaneously
|
195
|
+
product_list = [
|
196
|
+
"012345678901", "B08N5WRWNW", "045496590048",
|
197
|
+
"B07XJ8C8F5", "B09G9FPHY6"
|
198
|
+
]
|
199
|
+
|
200
|
+
batch_offers = client.get_current_offers_batch(product_list)
|
139
201
|
|
140
202
|
batch_offers.data.each do |identifier, offers|
|
141
|
-
|
203
|
+
next if offers.empty?
|
204
|
+
|
205
|
+
best_offer = offers.min_by(&:price)
|
206
|
+
puts "#{identifier}:"
|
207
|
+
puts " Best price: #{best_offer.retailer} - $#{best_offer.price}"
|
208
|
+
puts " Total offers: #{offers.length}"
|
209
|
+
puts " In stock: #{offers.count { |o| o.availability == 'in_stock' }}"
|
210
|
+
puts
|
142
211
|
end
|
143
212
|
```
|
144
213
|
|
145
|
-
### Price
|
214
|
+
### Historical Price Analysis
|
146
215
|
|
216
|
+
#### Comprehensive Price Trends
|
147
217
|
```ruby
|
148
|
-
|
218
|
+
require 'date'
|
219
|
+
|
220
|
+
# Get 90 days of price history for detailed analysis
|
221
|
+
end_date = Date.today
|
222
|
+
start_date = end_date - 90
|
223
|
+
|
149
224
|
history = client.get_price_history(
|
150
225
|
"012345678901",
|
151
|
-
"
|
152
|
-
"
|
226
|
+
start_date.strftime("%Y-%m-%d"),
|
227
|
+
end_date.strftime("%Y-%m-%d")
|
153
228
|
)
|
154
229
|
|
230
|
+
puts "š 90-Day Price Analysis"
|
231
|
+
puts "=" * 50
|
232
|
+
|
155
233
|
history.data.each do |offer|
|
156
|
-
|
157
|
-
|
158
|
-
|
159
|
-
|
160
|
-
|
234
|
+
next if offer.price_history.empty?
|
235
|
+
|
236
|
+
prices = offer.price_history.map(&:price)
|
237
|
+
current_price = offer.price
|
238
|
+
|
239
|
+
# Statistical analysis
|
240
|
+
avg_price = prices.sum.to_f / prices.length
|
241
|
+
min_price = prices.min
|
242
|
+
max_price = prices.max
|
243
|
+
|
244
|
+
# Price trend calculation
|
245
|
+
recent_prices = prices.last(7) # Last week
|
246
|
+
older_prices = prices.first([prices.length - 7, 1].max)
|
247
|
+
|
248
|
+
trend = if recent_prices.any? && older_prices.any?
|
249
|
+
recent_avg = recent_prices.sum.to_f / recent_prices.length
|
250
|
+
older_avg = older_prices.sum.to_f / older_prices.length
|
251
|
+
change_pct = ((recent_avg - older_avg) / older_avg * 100).round(1)
|
252
|
+
|
253
|
+
if change_pct > 5
|
254
|
+
"š Rising (+#{change_pct}%)"
|
255
|
+
elsif change_pct < -5
|
256
|
+
"š Falling (#{change_pct}%)"
|
257
|
+
else
|
258
|
+
"š Stable (#{change_pct}%)"
|
259
|
+
end
|
260
|
+
else
|
261
|
+
"š Insufficient data"
|
262
|
+
end
|
263
|
+
|
264
|
+
puts "šŖ #{offer.retailer.upcase}"
|
265
|
+
puts " Current: $#{current_price}"
|
266
|
+
puts " Average: $#{avg_price.round(2)}"
|
267
|
+
puts " Range: $#{min_price} - $#{max_price}"
|
268
|
+
puts " Savings opportunity: $#{(current_price - min_price).round(2)}"
|
269
|
+
puts " Trend: #{trend}"
|
270
|
+
puts " Data points: #{offer.price_history.length}"
|
271
|
+
puts
|
161
272
|
end
|
273
|
+
```
|
162
274
|
|
163
|
-
|
164
|
-
|
165
|
-
|
166
|
-
|
167
|
-
|
168
|
-
|
169
|
-
|
275
|
+
#### Retailer-Specific Historical Analysis
|
276
|
+
```ruby
|
277
|
+
# Compare price history across major retailers
|
278
|
+
retailers = %w[amazon walmart target bestbuy]
|
279
|
+
historical_comparison = {}
|
280
|
+
|
281
|
+
retailers.each do |retailer|
|
282
|
+
history = client.get_price_history(
|
283
|
+
"012345678901",
|
284
|
+
"2024-01-01",
|
285
|
+
"2024-12-31",
|
286
|
+
retailer: retailer
|
287
|
+
)
|
288
|
+
|
289
|
+
next if history.data.empty?
|
290
|
+
|
291
|
+
offer = history.data.first
|
292
|
+
if offer.price_history.any?
|
293
|
+
prices = offer.price_history.map(&:price)
|
294
|
+
historical_comparison[retailer] = {
|
295
|
+
current: offer.price,
|
296
|
+
average: prices.sum.to_f / prices.length,
|
297
|
+
lowest: prices.min,
|
298
|
+
highest: prices.max,
|
299
|
+
volatility: prices.max - prices.min
|
300
|
+
}
|
301
|
+
end
|
302
|
+
end
|
303
|
+
|
304
|
+
puts "Retailer Historical Comparison:"
|
305
|
+
historical_comparison.each do |retailer, data|
|
306
|
+
puts "#{retailer.capitalize}:"
|
307
|
+
puts " Current: $#{data[:current]}"
|
308
|
+
puts " Average: $#{data[:average].round(2)}"
|
309
|
+
puts " Best ever: $#{data[:lowest]}"
|
310
|
+
puts " Worst: $#{data[:highest]}"
|
311
|
+
puts " Volatility: $#{data[:volatility].round(2)}"
|
312
|
+
puts
|
313
|
+
end
|
170
314
|
```
|
171
315
|
|
172
316
|
### Product Monitoring
|
@@ -316,248 +460,477 @@ response_hash = product.to_h
|
|
316
460
|
puts response_hash[:data][:name]
|
317
461
|
```
|
318
462
|
|
319
|
-
##
|
463
|
+
## š Production Deployment
|
464
|
+
|
465
|
+
### Ruby on Rails Integration
|
320
466
|
|
321
|
-
### Price Comparison Tool
|
322
467
|
```ruby
|
323
|
-
|
324
|
-
|
325
|
-
|
326
|
-
|
327
|
-
|
328
|
-
|
468
|
+
# Gemfile
|
469
|
+
gem 'shopsavvy-sdk'
|
470
|
+
gem 'sidekiq' # For background jobs
|
471
|
+
|
472
|
+
# config/application.rb
|
473
|
+
config.shopsavvy_api_key = Rails.application.credentials.shopsavvy_api_key
|
474
|
+
|
475
|
+
# app/services/price_tracking_service.rb
|
476
|
+
class PriceTrackingService
|
477
|
+
def initialize
|
478
|
+
@client = ShopsavvyDataApi.new(
|
479
|
+
api_key: Rails.application.config.shopsavvy_api_key,
|
480
|
+
timeout: 60
|
481
|
+
)
|
329
482
|
end
|
483
|
+
|
484
|
+
def track_product(product_id, target_price)
|
485
|
+
# Schedule monitoring
|
486
|
+
@client.schedule_product_monitoring(product_id, 'daily')
|
487
|
+
|
488
|
+
# Create local tracking record
|
489
|
+
PriceAlert.create!(
|
490
|
+
product_identifier: product_id,
|
491
|
+
target_price: target_price,
|
492
|
+
status: 'active'
|
493
|
+
)
|
494
|
+
end
|
495
|
+
|
496
|
+
def check_price_alerts
|
497
|
+
PriceAlert.active.find_each do |alert|
|
498
|
+
CheckPriceAlertJob.perform_later(alert.id)
|
499
|
+
end
|
500
|
+
end
|
501
|
+
end
|
502
|
+
|
503
|
+
# app/jobs/check_price_alert_job.rb
|
504
|
+
class CheckPriceAlertJob < ApplicationJob
|
505
|
+
queue_as :default
|
330
506
|
|
331
|
-
|
332
|
-
|
333
|
-
|
334
|
-
|
335
|
-
|
336
|
-
|
337
|
-
|
338
|
-
|
339
|
-
|
340
|
-
|
341
|
-
|
342
|
-
|
343
|
-
|
507
|
+
def perform(alert_id)
|
508
|
+
alert = PriceAlert.find(alert_id)
|
509
|
+
client = ShopsavvyDataApi.new(api_key: Rails.application.config.shopsavvy_api_key)
|
510
|
+
|
511
|
+
offers = client.get_current_offers(alert.product_identifier)
|
512
|
+
best_offer = offers.data.min_by(&:price)
|
513
|
+
|
514
|
+
if best_offer && best_offer.price <= alert.target_price
|
515
|
+
# Send notification
|
516
|
+
PriceAlertMailer.target_reached(alert, best_offer).deliver_now
|
517
|
+
alert.update!(status: 'triggered', triggered_at: Time.current)
|
518
|
+
end
|
519
|
+
rescue ShopsavvyDataApi::Error => e
|
520
|
+
Rails.logger.error "ShopSavvy API error: #{e.message}"
|
521
|
+
# Optionally retry or alert administrators
|
522
|
+
end
|
344
523
|
end
|
524
|
+
```
|
345
525
|
|
346
|
-
|
347
|
-
|
348
|
-
|
526
|
+
### Sinatra Microservice
|
527
|
+
|
528
|
+
```ruby
|
529
|
+
# app.rb
|
530
|
+
require 'sinatra'
|
531
|
+
require 'json'
|
532
|
+
require 'shopsavvy_data_api'
|
533
|
+
|
534
|
+
class PriceAPI < Sinatra::Base
|
535
|
+
configure do
|
536
|
+
set :shopsavvy_client, ShopsavvyDataApi.new(
|
537
|
+
api_key: ENV['SHOPSAVVY_API_KEY'],
|
538
|
+
timeout: 30
|
539
|
+
)
|
540
|
+
end
|
541
|
+
|
542
|
+
before do
|
543
|
+
content_type :json
|
544
|
+
end
|
545
|
+
|
546
|
+
get '/api/product/:identifier/price' do
|
547
|
+
identifier = params[:identifier]
|
548
|
+
|
549
|
+
begin
|
550
|
+
offers = settings.shopsavvy_client.get_current_offers(identifier)
|
551
|
+
|
552
|
+
{
|
553
|
+
success: true,
|
554
|
+
product_id: identifier,
|
555
|
+
offers: offers.data.map do |offer|
|
556
|
+
{
|
557
|
+
retailer: offer.retailer,
|
558
|
+
price: offer.price,
|
559
|
+
availability: offer.availability,
|
560
|
+
condition: offer.condition,
|
561
|
+
url: offer.url
|
562
|
+
}
|
563
|
+
end,
|
564
|
+
best_price: offers.data.min_by(&:price)&.price,
|
565
|
+
credits_remaining: offers.credits_remaining
|
566
|
+
}.to_json
|
567
|
+
rescue ShopsavvyDataApi::Error => e
|
568
|
+
status 400
|
569
|
+
{ success: false, error: e.message }.to_json
|
570
|
+
end
|
571
|
+
end
|
572
|
+
|
573
|
+
get '/api/product/:identifier/history' do
|
574
|
+
identifier = params[:identifier]
|
575
|
+
days = (params[:days] || 30).to_i
|
576
|
+
|
577
|
+
end_date = Date.today
|
578
|
+
start_date = end_date - days
|
579
|
+
|
580
|
+
begin
|
581
|
+
history = settings.shopsavvy_client.get_price_history(
|
582
|
+
identifier,
|
583
|
+
start_date.strftime('%Y-%m-%d'),
|
584
|
+
end_date.strftime('%Y-%m-%d')
|
585
|
+
)
|
586
|
+
|
587
|
+
{
|
588
|
+
success: true,
|
589
|
+
product_id: identifier,
|
590
|
+
period: "#{days} days",
|
591
|
+
data: history.data
|
592
|
+
}.to_json
|
593
|
+
rescue ShopsavvyDataApi::Error => e
|
594
|
+
status 400
|
595
|
+
{ success: false, error: e.message }.to_json
|
596
|
+
end
|
597
|
+
end
|
598
|
+
end
|
349
599
|
```
|
350
600
|
|
351
|
-
###
|
601
|
+
### Background Processing with Sidekiq
|
602
|
+
|
352
603
|
```ruby
|
353
|
-
|
354
|
-
|
355
|
-
|
356
|
-
|
357
|
-
|
358
|
-
|
359
|
-
|
360
|
-
|
361
|
-
|
362
|
-
|
363
|
-
|
364
|
-
|
365
|
-
|
366
|
-
|
367
|
-
|
368
|
-
|
369
|
-
|
370
|
-
|
604
|
+
# lib/price_monitor.rb
|
605
|
+
class PriceMonitor
|
606
|
+
include Sidekiq::Worker
|
607
|
+
sidekiq_options retry: 3, backtrace: true
|
608
|
+
|
609
|
+
def perform(product_ids)
|
610
|
+
client = ShopsavvyDataApi.new(api_key: ENV['SHOPSAVVY_API_KEY'])
|
611
|
+
|
612
|
+
product_ids.each do |product_id|
|
613
|
+
begin
|
614
|
+
# Get current prices
|
615
|
+
offers = client.get_current_offers(product_id)
|
616
|
+
next if offers.data.empty?
|
617
|
+
|
618
|
+
# Store in database or cache
|
619
|
+
best_price = offers.data.min_by(&:price).price
|
620
|
+
Redis.current.setex("price:#{product_id}", 3600, best_price)
|
621
|
+
|
622
|
+
# Check for alerts
|
623
|
+
check_price_alerts(product_id, best_price)
|
624
|
+
|
625
|
+
rescue ShopsavvyDataApi::RateLimitError => e
|
626
|
+
# Exponential backoff
|
627
|
+
self.class.perform_in(2 ** sidekiq_options['retry_count'], [product_id])
|
628
|
+
raise e
|
629
|
+
rescue ShopsavvyDataApi::Error => e
|
630
|
+
logger.error "API error for #{product_id}: #{e.message}"
|
631
|
+
end
|
632
|
+
end
|
633
|
+
end
|
634
|
+
|
635
|
+
private
|
636
|
+
|
637
|
+
def check_price_alerts(product_id, current_price)
|
638
|
+
# Implementation for checking and triggering alerts
|
371
639
|
end
|
372
640
|
end
|
373
641
|
|
374
|
-
#
|
375
|
-
|
376
|
-
setup_price_alert(client, "012345678901", 299.99)
|
642
|
+
# Schedule regular monitoring
|
643
|
+
PriceMonitor.perform_async(['012345678901', 'B08N5WRWNW'])
|
377
644
|
```
|
378
645
|
|
379
|
-
|
646
|
+
## š” Real-World Use Cases
|
647
|
+
|
648
|
+
### E-commerce Price Intelligence
|
380
649
|
```ruby
|
381
|
-
|
650
|
+
# Comprehensive competitive analysis tool
|
651
|
+
class CompetitiveAnalyzer
|
652
|
+
def initialize(api_key)
|
653
|
+
@client = ShopsavvyDataApi.new(api_key: api_key)
|
654
|
+
end
|
382
655
|
|
383
|
-
def
|
384
|
-
|
385
|
-
start_date = end_date - days
|
386
|
-
|
387
|
-
history = client.get_price_history(
|
388
|
-
identifier,
|
389
|
-
start_date.strftime("%Y-%m-%d"),
|
390
|
-
end_date.strftime("%Y-%m-%d")
|
391
|
-
)
|
392
|
-
|
393
|
-
analysis = {}
|
394
|
-
|
395
|
-
history.data.each do |offer|
|
396
|
-
next if offer.price_history.empty?
|
656
|
+
def analyze_market(product_ids, competitors = %w[amazon walmart target bestbuy])
|
657
|
+
analysis = {}
|
397
658
|
|
398
|
-
|
659
|
+
product_ids.each do |product_id|
|
660
|
+
product_analysis = analyze_product_competition(product_id, competitors)
|
661
|
+
analysis[product_id] = product_analysis
|
662
|
+
end
|
399
663
|
|
400
|
-
|
401
|
-
|
402
|
-
|
664
|
+
generate_competitive_report(analysis)
|
665
|
+
end
|
666
|
+
|
667
|
+
private
|
668
|
+
|
669
|
+
def analyze_product_competition(product_id, competitors)
|
670
|
+
# Get product details
|
671
|
+
product = @client.get_product_details(product_id)
|
403
672
|
|
404
|
-
|
405
|
-
|
406
|
-
|
407
|
-
|
408
|
-
|
409
|
-
|
410
|
-
|
673
|
+
# Get current offers from all retailers
|
674
|
+
all_offers = @client.get_current_offers(product_id)
|
675
|
+
|
676
|
+
# Filter by target competitors
|
677
|
+
competitor_offers = all_offers.data.select do |offer|
|
678
|
+
competitors.include?(offer.retailer.downcase)
|
679
|
+
end
|
680
|
+
|
681
|
+
# Price analysis
|
682
|
+
prices = competitor_offers.map(&:price)
|
411
683
|
|
412
|
-
|
413
|
-
|
414
|
-
|
415
|
-
|
416
|
-
|
417
|
-
|
418
|
-
|
684
|
+
{
|
685
|
+
product_name: product.data.name,
|
686
|
+
brand: product.data.brand,
|
687
|
+
total_offers: all_offers.data.length,
|
688
|
+
competitor_offers: competitor_offers.length,
|
689
|
+
price_range: {
|
690
|
+
min: prices.min,
|
691
|
+
max: prices.max,
|
692
|
+
average: prices.sum.to_f / prices.length
|
693
|
+
},
|
694
|
+
market_position: calculate_market_position(competitor_offers),
|
695
|
+
availability_score: calculate_availability_score(competitor_offers)
|
419
696
|
}
|
420
697
|
end
|
421
|
-
|
422
|
-
|
698
|
+
|
699
|
+
def calculate_market_position(offers)
|
700
|
+
return 'No data' if offers.empty?
|
701
|
+
|
702
|
+
prices = offers.map(&:price).sort
|
703
|
+
median_price = prices[prices.length / 2]
|
704
|
+
|
705
|
+
case median_price
|
706
|
+
when 0..50 then 'Budget'
|
707
|
+
when 50..200 then 'Mid-range'
|
708
|
+
when 200..500 then 'Premium'
|
709
|
+
else 'Luxury'
|
710
|
+
end
|
711
|
+
end
|
712
|
+
|
713
|
+
def calculate_availability_score(offers)
|
714
|
+
return 0 if offers.empty?
|
715
|
+
|
716
|
+
in_stock_count = offers.count { |offer| offer.availability == 'in_stock' }
|
717
|
+
(in_stock_count.to_f / offers.length * 100).round(1)
|
718
|
+
end
|
423
719
|
end
|
424
720
|
|
425
721
|
# Usage
|
426
|
-
|
427
|
-
|
428
|
-
|
429
|
-
|
430
|
-
puts "#{retailer}:"
|
431
|
-
puts " Current: $#{data[:current_price]}"
|
432
|
-
puts " Average: $#{data[:average_price].round(2)}"
|
433
|
-
puts " Range: $#{data[:min_price]} - $#{data[:max_price]}"
|
434
|
-
puts " Trend: #{data[:trend]}"
|
435
|
-
puts
|
436
|
-
end
|
722
|
+
analyzer = CompetitiveAnalyzer.new(ENV['SHOPSAVVY_API_KEY'])
|
723
|
+
report = analyzer.analyze_market([
|
724
|
+
'012345678901', 'B08N5WRWNW', '045496590048'
|
725
|
+
])
|
437
726
|
```
|
438
727
|
|
439
|
-
###
|
728
|
+
### Inventory Management Integration
|
440
729
|
```ruby
|
441
|
-
|
442
|
-
|
443
|
-
|
444
|
-
|
445
|
-
|
446
|
-
|
447
|
-
|
448
|
-
|
449
|
-
|
450
|
-
|
451
|
-
|
452
|
-
|
730
|
+
# Integration with inventory management system
|
731
|
+
class InventoryPriceManager
|
732
|
+
def initialize(api_key)
|
733
|
+
@client = ShopsavvyDataApi.new(api_key: api_key)
|
734
|
+
end
|
735
|
+
|
736
|
+
def update_competitive_pricing(inventory_items)
|
737
|
+
pricing_updates = []
|
738
|
+
|
739
|
+
inventory_items.each do |item|
|
740
|
+
next unless item.competitor_tracking_enabled?
|
741
|
+
|
742
|
+
begin
|
743
|
+
# Get current market prices
|
744
|
+
offers = @client.get_current_offers(item.barcode)
|
745
|
+
next if offers.data.empty?
|
746
|
+
|
747
|
+
# Calculate competitive price point
|
748
|
+
competitor_prices = offers.data.map(&:price)
|
749
|
+
market_analysis = analyze_market_prices(competitor_prices)
|
750
|
+
|
751
|
+
suggested_price = calculate_competitive_price(
|
752
|
+
item.cost_price,
|
753
|
+
market_analysis,
|
754
|
+
item.target_margin
|
755
|
+
)
|
756
|
+
|
757
|
+
pricing_updates << {
|
758
|
+
item_id: item.id,
|
759
|
+
current_price: item.selling_price,
|
760
|
+
suggested_price: suggested_price,
|
761
|
+
market_analysis: market_analysis,
|
762
|
+
reasoning: generate_pricing_reasoning(item, market_analysis, suggested_price)
|
763
|
+
}
|
764
|
+
|
765
|
+
rescue ShopsavvyDataApi::Error => e
|
766
|
+
Rails.logger.error "Pricing update failed for #{item.id}: #{e.message}"
|
767
|
+
next
|
768
|
+
end
|
453
769
|
end
|
770
|
+
|
771
|
+
pricing_updates
|
454
772
|
end
|
455
|
-
|
456
|
-
|
457
|
-
|
458
|
-
|
459
|
-
|
460
|
-
|
461
|
-
|
773
|
+
|
774
|
+
private
|
775
|
+
|
776
|
+
def analyze_market_prices(prices)
|
777
|
+
sorted_prices = prices.sort
|
778
|
+
{
|
779
|
+
min: sorted_prices.first,
|
780
|
+
max: sorted_prices.last,
|
781
|
+
median: sorted_prices[sorted_prices.length / 2],
|
782
|
+
average: prices.sum.to_f / prices.length,
|
783
|
+
percentile_25: sorted_prices[(sorted_prices.length * 0.25).round],
|
784
|
+
percentile_75: sorted_prices[(sorted_prices.length * 0.75).round]
|
785
|
+
}
|
462
786
|
end
|
463
|
-
|
464
|
-
{ successful: successful, failed: failed }
|
465
|
-
end
|
466
787
|
|
467
|
-
|
468
|
-
|
469
|
-
|
470
|
-
|
471
|
-
|
472
|
-
|
473
|
-
|
474
|
-
setup_bulk_monitoring(client, products_to_monitor, "daily")
|
788
|
+
def calculate_competitive_price(cost_price, market_analysis, target_margin)
|
789
|
+
min_price = cost_price * (1 + target_margin)
|
790
|
+
competitive_price = market_analysis[:percentile_25] * 0.95 # 5% under 25th percentile
|
791
|
+
|
792
|
+
[min_price, competitive_price].max.round(2)
|
793
|
+
end
|
794
|
+
end
|
475
795
|
```
|
476
796
|
|
477
|
-
###
|
797
|
+
### Market Research Analytics
|
478
798
|
```ruby
|
479
|
-
#
|
480
|
-
class
|
481
|
-
|
482
|
-
|
483
|
-
|
484
|
-
|
485
|
-
|
799
|
+
# Advanced market research and trend analysis
|
800
|
+
class MarketResearcher
|
801
|
+
def initialize(api_key)
|
802
|
+
@client = ShopsavvyDataApi.new(api_key: api_key)
|
803
|
+
end
|
804
|
+
|
805
|
+
def research_category_trends(product_categories, time_periods)
|
806
|
+
research_report = {}
|
486
807
|
|
487
|
-
|
488
|
-
|
808
|
+
product_categories.each do |category, product_list|
|
809
|
+
category_data = analyze_category_trends(product_list, time_periods)
|
810
|
+
research_report[category] = category_data
|
489
811
|
end
|
812
|
+
|
813
|
+
generate_market_intelligence_report(research_report)
|
490
814
|
end
|
491
|
-
|
492
|
-
def
|
493
|
-
|
494
|
-
best_offer = offers.data.min_by(&:price)
|
815
|
+
|
816
|
+
def track_seasonal_patterns(product_id, months = 12)
|
817
|
+
patterns = {}
|
495
818
|
|
496
|
-
|
497
|
-
|
498
|
-
|
499
|
-
|
819
|
+
(0...months).each do |month_offset|
|
820
|
+
end_date = Date.today - (month_offset * 30)
|
821
|
+
start_date = end_date - 30
|
822
|
+
|
823
|
+
history = @client.get_price_history(
|
824
|
+
product_id,
|
825
|
+
start_date.strftime('%Y-%m-%d'),
|
826
|
+
end_date.strftime('%Y-%m-%d')
|
827
|
+
)
|
828
|
+
|
829
|
+
month_name = end_date.strftime('%B %Y')
|
830
|
+
patterns[month_name] = analyze_monthly_patterns(history.data)
|
500
831
|
end
|
501
|
-
|
502
|
-
|
832
|
+
|
833
|
+
identify_seasonal_trends(patterns)
|
503
834
|
end
|
504
|
-
end
|
505
835
|
|
506
|
-
|
507
|
-
|
836
|
+
private
|
837
|
+
|
838
|
+
def analyze_monthly_patterns(history_data)
|
839
|
+
return { average_price: 0, volatility: 0 } if history_data.empty?
|
840
|
+
|
841
|
+
all_prices = []
|
842
|
+
|
843
|
+
history_data.each do |offer|
|
844
|
+
next if offer.price_history.empty?
|
845
|
+
all_prices.concat(offer.price_history.map(&:price))
|
846
|
+
end
|
847
|
+
|
848
|
+
return { average_price: 0, volatility: 0 } if all_prices.empty?
|
849
|
+
|
850
|
+
average = all_prices.sum.to_f / all_prices.length
|
851
|
+
variance = all_prices.map { |price| (price - average) ** 2 }.sum / all_prices.length
|
852
|
+
|
853
|
+
{
|
854
|
+
average_price: average.round(2),
|
855
|
+
volatility: Math.sqrt(variance).round(2),
|
856
|
+
price_points: all_prices.length
|
857
|
+
}
|
858
|
+
end
|
859
|
+
end
|
508
860
|
```
|
509
861
|
|
510
|
-
## š ļø Development
|
862
|
+
## š ļø Development & Testing
|
511
863
|
|
512
|
-
###
|
864
|
+
### Local Development Setup
|
513
865
|
|
514
|
-
```
|
515
|
-
|
516
|
-
|
866
|
+
```ruby
|
867
|
+
# Clone the repository
|
868
|
+
git clone https://github.com/shopsavvy/sdk-ruby.git
|
869
|
+
cd sdk-ruby
|
870
|
+
|
871
|
+
# Install dependencies
|
517
872
|
bundle install
|
518
|
-
```
|
519
873
|
|
520
|
-
|
874
|
+
# Set up environment variables
|
875
|
+
echo 'SHOPSAVVY_API_KEY=ss_test_your_test_key_here' > .env
|
521
876
|
|
522
|
-
|
523
|
-
# Run all tests
|
877
|
+
# Run tests
|
524
878
|
bundle exec rspec
|
525
879
|
|
526
|
-
# Run
|
527
|
-
bundle exec
|
528
|
-
|
529
|
-
# Run specific test file
|
530
|
-
bundle exec rspec spec/client_spec.rb
|
880
|
+
# Run linting
|
881
|
+
bundle exec rubocop
|
531
882
|
|
532
|
-
#
|
533
|
-
bundle exec
|
883
|
+
# Generate documentation
|
884
|
+
bundle exec yard doc
|
534
885
|
```
|
535
886
|
|
536
|
-
###
|
537
|
-
|
538
|
-
```bash
|
539
|
-
# Linting
|
540
|
-
bundle exec rubocop
|
887
|
+
### Testing Your Integration
|
541
888
|
|
542
|
-
|
543
|
-
|
889
|
+
```ruby
|
890
|
+
# Create a test script
|
891
|
+
require 'shopsavvy_data_api'
|
544
892
|
|
545
|
-
#
|
546
|
-
|
893
|
+
# Use test API key (starts with ss_test_)
|
894
|
+
client = ShopsavvyDataApi.new(api_key: 'ss_test_your_test_key_here')
|
547
895
|
|
548
|
-
#
|
549
|
-
|
896
|
+
# Test basic functionality
|
897
|
+
begin
|
898
|
+
# Test product lookup
|
899
|
+
product = client.get_product_details('012345678901')
|
900
|
+
puts "ā
Product lookup: #{product.data.name}"
|
901
|
+
|
902
|
+
# Test current offers
|
903
|
+
offers = client.get_current_offers('012345678901')
|
904
|
+
puts "ā
Current offers: #{offers.data.length} found"
|
905
|
+
|
906
|
+
# Test usage info
|
907
|
+
usage = client.get_usage
|
908
|
+
puts "ā
API usage: #{usage.data.credits_remaining} credits remaining"
|
909
|
+
|
910
|
+
puts "\nš All tests passed! SDK is working correctly."
|
911
|
+
|
912
|
+
rescue ShopsavvyDataApi::Error => e
|
913
|
+
puts "ā Test failed: #{e.message}"
|
914
|
+
end
|
550
915
|
```
|
551
916
|
|
552
|
-
|
917
|
+
## š Additional Resources
|
553
918
|
|
554
|
-
|
919
|
+
- **[ShopSavvy Data API Documentation](https://shopsavvy.com/data/documentation)** - Complete API reference
|
920
|
+
- **[API Dashboard](https://shopsavvy.com/data/dashboard)** - Manage your API keys and usage
|
921
|
+
- **[GitHub Repository](https://github.com/shopsavvy/sdk-ruby)** - Source code and issues
|
922
|
+
- **[RubyGems Page](https://rubygems.org/gems/shopsavvy-sdk)** - Gem releases and stats
|
923
|
+
- **[Support](mailto:business@shopsavvy.com)** - Get help from our team
|
555
924
|
|
556
|
-
##
|
925
|
+
## š¤ Contributing
|
926
|
+
|
927
|
+
We welcome contributions! Please see our [Contributing Guide](CONTRIBUTING.md) for details on:
|
557
928
|
|
558
|
-
-
|
559
|
-
-
|
560
|
-
-
|
929
|
+
- Reporting bugs
|
930
|
+
- Suggesting enhancements
|
931
|
+
- Submitting pull requests
|
932
|
+
- Development workflow
|
933
|
+
- Code standards
|
561
934
|
|
562
935
|
## š License
|
563
936
|
|
@@ -565,10 +938,23 @@ This project is licensed under the MIT License - see the [LICENSE](LICENSE) file
|
|
565
938
|
|
566
939
|
## š¢ About ShopSavvy
|
567
940
|
|
568
|
-
ShopSavvy is
|
941
|
+
**ShopSavvy** is the world's first mobile shopping app, helping consumers find the best deals since 2008. With over **40 million downloads** and millions of active users, ShopSavvy has saved consumers billions of dollars.
|
942
|
+
|
943
|
+
### Our Data API Powers:
|
944
|
+
- š **E-commerce platforms** with competitive intelligence
|
945
|
+
- š **Market research** with real-time pricing data
|
946
|
+
- šŖ **Retailers** with inventory and pricing optimization
|
947
|
+
- š± **Mobile apps** with product lookup and price comparison
|
948
|
+
- š¤ **Business intelligence** with automated price monitoring
|
569
949
|
|
570
|
-
|
950
|
+
### Why Choose ShopSavvy Data API?
|
951
|
+
- ā
**Trusted by millions** - Proven at scale since 2008
|
952
|
+
- ā
**Comprehensive coverage** - 1000+ retailers, millions of products
|
953
|
+
- ā
**Real-time accuracy** - Fresh data updated continuously
|
954
|
+
- ā
**Developer-friendly** - Easy integration, great documentation
|
955
|
+
- ā
**Reliable infrastructure** - 99.9% uptime, enterprise-grade
|
956
|
+
- ā
**Flexible pricing** - Plans for every use case and budget
|
571
957
|
|
572
958
|
---
|
573
959
|
|
574
|
-
**
|
960
|
+
**Ready to get started?** [Sign up for your API key](https://shopsavvy.com/data) ⢠**Need help?** [Contact us](mailto:business@shopsavvy.com)
|
metadata
CHANGED
@@ -1,11 +1,10 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: shopsavvy-sdk
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 1.0.
|
4
|
+
version: 1.0.1
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- ShopSavvy by Monolith Technologies, Inc.
|
8
|
-
autorequire:
|
9
8
|
bindir: exe
|
10
9
|
cert_chain: []
|
11
10
|
date: 2025-07-30 00:00:00.000000000 Z
|
@@ -2927,7 +2926,6 @@ metadata:
|
|
2927
2926
|
changelog_uri: https://github.com/shopsavvy/sdk-ruby/blob/main/CHANGELOG.md
|
2928
2927
|
documentation_uri: https://shopsavvy.com/data/documentation
|
2929
2928
|
bug_tracker_uri: https://github.com/shopsavvy/sdk-ruby/issues
|
2930
|
-
post_install_message:
|
2931
2929
|
rdoc_options: []
|
2932
2930
|
require_paths:
|
2933
2931
|
- lib
|
@@ -2942,8 +2940,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
2942
2940
|
- !ruby/object:Gem::Version
|
2943
2941
|
version: '0'
|
2944
2942
|
requirements: []
|
2945
|
-
rubygems_version: 3.
|
2946
|
-
signing_key:
|
2943
|
+
rubygems_version: 3.6.2
|
2947
2944
|
specification_version: 4
|
2948
2945
|
summary: Official Ruby SDK for ShopSavvy Data API
|
2949
2946
|
test_files: []
|