tophatter-merchant 1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 8de1362487600774b5ef62b300902aa00bcbd078
4
+ data.tar.gz: d43f56863dc7cf280b01a85c0a5b4a3fb7de54d6
5
+ SHA512:
6
+ metadata.gz: ceacd0c967b71400e9c001b6979a804f1f92b4ca18d236fc130c12c24eaac066f6220fe702e902c1de2ccc984d97301c91e7b6586a8422c9f1cfad292b7f0967
7
+ data.tar.gz: 3d077e9be766d053332a4708bcfe29b78b4c9398000dbd08fa23b82a6ae55f4f0f3433a7c3234caadbcc52425a588993060185191a0c73c562fa2e8949edc1b3
@@ -0,0 +1,7 @@
1
+ Gemfile.lock
2
+ .DS_Store
3
+ *.swp
4
+ /.bundle
5
+ .idea
6
+ .api_path
7
+ .access_token
data/.rspec ADDED
@@ -0,0 +1,2 @@
1
+ --color
2
+ --require spec_helper
@@ -0,0 +1,3 @@
1
+ === 2.1.10 2016-01-28
2
+
3
+ * Created GitHub project with initial code.
data/Gemfile ADDED
@@ -0,0 +1,9 @@
1
+ source 'https://rubygems.org'
2
+ gemspec
3
+
4
+ group :development, :test do
5
+ gem 'awesome_print'
6
+ gem 'pry'
7
+ gem 'rspec'
8
+ gem 'webmock'
9
+ end
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License
2
+
3
+ Copyright (c) 2016 Tophatter, Inc
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
@@ -0,0 +1,120 @@
1
+ ### Tophatter Merchant API
2
+ Full documentation is available [here](https://tophatter.readme.io/v1/docs).
3
+
4
+ ### Console Testing (Development)
5
+ ```bash
6
+ echo http://tophatter.dev/merchant_api/v1 > .api_host
7
+ echo a42880c4b7d0809300bed27c453f883d > .access_token
8
+ bin/console
9
+ ```
10
+
11
+ ### API Authentication
12
+ ```ruby
13
+ TophatterMerchant.access_token = <YOUR ACCESS TOKEN>
14
+ ```
15
+
16
+ ### Products
17
+
18
+ #### List the product definition.
19
+ ```ruby
20
+ TophatterMerchant::Product.schema
21
+ ```
22
+
23
+ #### List all of the products in your inventory.
24
+ ```ruby
25
+ TophatterMerchant::Product.all(page: 1, per_page: 100)
26
+ ```
27
+
28
+ #### Search for products.
29
+ ```ruby
30
+ TophatterMerchant::Product.search(query: 'Shirt', page: 1, per_page: 100)
31
+ ```
32
+
33
+ #### Retrieve a specific product.
34
+ ```ruby
35
+ TophatterMerchant::Product.retrieve('WAH5282')
36
+ ```
37
+
38
+ #### Create a product.
39
+ ```ruby
40
+ TophatterMerchant::Product.create(
41
+ identifier: '6631A',
42
+ category: 'Electronics|Hardware (Computers/Tablets/Phones)|Mobile',
43
+ title: 'Apple iPhone 6S',
44
+ description: 'This is the description.',
45
+ condition: 'Refurbished - Manufacturer',
46
+ starting_bid: 1,
47
+ buy_now_price: 150,
48
+ cost_basis: 75,
49
+ shipping_price: 5.0,
50
+ shipping_origin: 'United States',
51
+ weight: 6,
52
+ days_to_fulfill: 3,
53
+ days_to_deliver: 5,
54
+ variations: {
55
+ '6631A-WHITE': { identifier: '6631A-WHITE', color: 'White', quantity: 1 },
56
+ '6631A-BLACK': { identifier: '6631A-BLACK', color: 'Black', quantity: 2 }
57
+ },
58
+ primary_image: 'https://d38eepresuu519.cloudfront.net/b2aa7d2708324f756ffee551ba43a74f/original.jpg',
59
+ extra_images: 'https://d38eepresuu519.cloudfront.net/e615c55184c06f391dbd768f855904e6/original.jpg|https://d38eepresuu519.cloudfront.net/7cd125f0fa42c965675eabaf3309aa6d/original.jpg'
60
+ )
61
+ ```
62
+
63
+ #### Update a product.
64
+ ```ruby
65
+ TophatterMerchant::Product.update('6631A', buy_now_price: 100)
66
+ ```
67
+
68
+ #### Disable an enabled product.
69
+ ```ruby
70
+ TophatterMerchant::Product.disable('6631A')
71
+ ```
72
+
73
+ #### Enable a disabled product.
74
+ ```ruby
75
+ TophatterMerchant::Product.enable('6631A')
76
+ ```
77
+
78
+ ### Variations
79
+
80
+ #### Retrieve a specific variation.
81
+ ```ruby
82
+ TophatterMerchant::Variation.retrieve('6631A-BLACK')
83
+ ```
84
+
85
+ #### Create a variation.
86
+ ```ruby
87
+ TophatterMerchant::Variation.create(product_identifier: '6631A', identifier: '6631A-PINK', color: 'Pink', quantity: 33)
88
+ ```
89
+
90
+ #### Update a variation.
91
+ ```ruby
92
+ TophatterMerchant::Variation.update('6631A-PINK', quantity: 999)
93
+ ```
94
+
95
+ ### Orders
96
+
97
+ #### List all orders.
98
+ ```ruby
99
+ TophatterMerchant::Order.all(filter: 'unfulfilled', page: 1, per_page: 100)
100
+ ```
101
+
102
+ #### Retrieve a specific order.
103
+ ```ruby
104
+ TophatterMerchant::Order.retrieve(1035509247)
105
+ ```
106
+
107
+ #### Fulfill an order.
108
+ ```ruby
109
+ TophatterMerchant::Order.fulfill(1035509247, carrier: 'USPS', tracking_number: '9400111899562173406594')
110
+ ```
111
+
112
+ #### Refund the shipping fee for an order.
113
+ ```ruby
114
+ TophatterMerchant::Order.refund(1035509249, type: 'partial', reason: 'delay_in_shipping', fees: ['shipping_fee'])
115
+ ```
116
+
117
+ #### Refund an order in full.
118
+ ```ruby
119
+ TophatterMerchant::Order.refund(1035509249, type: 'full', reason: 'out_of_stock')
120
+ ```
data/VERSION ADDED
@@ -0,0 +1 @@
1
+ 1.0
@@ -0,0 +1,23 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'bundler/setup'
4
+ require 'rest-client'
5
+ require 'tophatter_merchant'
6
+ require 'awesome_print'
7
+ require 'pry'
8
+
9
+ begin
10
+ TophatterMerchant.api_path = File.read('.api_path').chomp
11
+ rescue Errno::ENOENT
12
+ end
13
+
14
+ begin
15
+ TophatterMerchant.access_token = File.read('.access_token').chomp
16
+ rescue Errno::ENOENT
17
+ end
18
+
19
+ puts "TophatterMerchant.api_path: #{TophatterMerchant.api_path}"
20
+ puts "TophatterMerchant.access_token: #{TophatterMerchant.access_token}"
21
+
22
+ Pry.config.color = false
23
+ Pry.start
@@ -0,0 +1,34 @@
1
+ require 'active_model'
2
+
3
+ require File.dirname(__FILE__) + '/tophatter_merchant/version'
4
+ require File.dirname(__FILE__) + '/tophatter_merchant/exceptions'
5
+
6
+ # Resources:
7
+ require File.dirname(__FILE__) + '/tophatter_merchant/resource'
8
+ require File.dirname(__FILE__) + '/tophatter_merchant/account'
9
+ require File.dirname(__FILE__) + '/tophatter_merchant/api_key'
10
+ require File.dirname(__FILE__) + '/tophatter_merchant/image'
11
+ require File.dirname(__FILE__) + '/tophatter_merchant/metadata'
12
+ require File.dirname(__FILE__) + '/tophatter_merchant/order'
13
+ require File.dirname(__FILE__) + '/tophatter_merchant/product'
14
+ require File.dirname(__FILE__) + '/tophatter_merchant/variation'
15
+
16
+ module TophatterMerchant
17
+
18
+ def self.access_token
19
+ defined?(@@access_token) ? @@access_token : nil
20
+ end
21
+
22
+ def self.access_token=(token)
23
+ @@access_token = token
24
+ end
25
+
26
+ def self.api_path
27
+ defined?(@@api_path) ? @@api_path : 'https://tophatter.com/merchant_api/v1'
28
+ end
29
+
30
+ def self.api_path=(path)
31
+ @@api_path = path
32
+ end
33
+
34
+ end
@@ -0,0 +1,53 @@
1
+ module TophatterMerchant
2
+ class Account < Resource
3
+
4
+ attr_accessor :access_token, :first_name, :last_name, :store_name, :email, :country, :time_zone
5
+
6
+ def id
7
+ email
8
+ end
9
+
10
+ class << self
11
+
12
+ # ap TophatterMerchant::Account.schema
13
+ def schema
14
+ get(url: "#{path}/schema.json")
15
+ end
16
+
17
+ # ap TophatterMerchant::Account.authenticate(email: 'megatron@autobot.com', password: 'ipipip').to_h
18
+ def authenticate(email:, password:)
19
+ Account.new post(url: "#{path}/authenticate.json", params: {
20
+ email: email,
21
+ password: password
22
+ })
23
+ end
24
+
25
+ # ap TophatterMerchant::Account.me.to_h
26
+ def me
27
+ Account.new get(url: "#{path}/me.json")
28
+ end
29
+
30
+ # ap TophatterMerchant::Account.create(first_name: 'Foo', last_name: 'Bar', store_name: 'Foo Bar, Inc', email: 'foo@bar.com', password: 'ipipip', country: 'United States', time_zone: 'Pacific Time (US & Canada)').to_h
31
+ def create(data)
32
+ Account.new post(url: "#{path}.json", params: data)
33
+ end
34
+
35
+ # Change first & last name:
36
+ # ap TophatterMerchant::Account.update(first_name: 'Mega', last_name: 'Tron').to_h
37
+ # Change password:
38
+ # ap TophatterMerchant::Account.update(password: 'qwer1234').to_h
39
+ # ap TophatterMerchant::Account.authenticate(email: 'megatron@autobot.com', password: 'qwer1234').to_h
40
+ # ap TophatterMerchant::Account.update(password: 'ipipip').to_h
41
+ def update(data)
42
+ Account.new post(url: "#{path}/update.json", params: data)
43
+ end
44
+
45
+ protected
46
+
47
+ def path
48
+ super + '/account'
49
+ end
50
+
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,45 @@
1
+ module TophatterMerchant
2
+ class ApiKey < Resource
3
+
4
+ attr_accessor :id, :access_token, :created_at
5
+
6
+ class << self
7
+
8
+ # ap TophatterMerchant::ApiKey.schema
9
+ def schema
10
+ get(url: "#{path}/schema.json")
11
+ end
12
+
13
+ # ap TophatterMerchant::ApiKey.all.map(&:to_h)
14
+ def all
15
+ get(url: "#{path}.json").map do |hash|
16
+ ApiKey.new(hash)
17
+ end
18
+ end
19
+
20
+ # ap TophatterMerchant::ApiKey.retrieve(1).to_h
21
+ def retrieve(id)
22
+ ApiKey.new get(url: "#{path}/#{id}.json")
23
+ end
24
+
25
+ # ap TophatterMerchant::ApiKey.create.to_h
26
+ def create
27
+ ApiKey.new post(url: "#{path}.json")
28
+ end
29
+
30
+ # ap TophatterMerchant::ApiKey.destroy(TophatterMerchant::ApiKey.all.last.id).map(&:to_h)
31
+ def destroy(id)
32
+ delete(url: "#{path}/#{id}.json").map do |hash|
33
+ ApiKey.new(hash)
34
+ end
35
+ end
36
+
37
+ protected
38
+
39
+ def path
40
+ super + '/api_keys'
41
+ end
42
+
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,8 @@
1
+ module TophatterMerchant
2
+ class BaseException < StandardError; end
3
+ class BadContentTypeException < BaseException; end
4
+ class UnauthorizedException < BaseException; end
5
+ class BadRequestException < BaseException; end
6
+ class NotFoundException < BaseException; end
7
+ class ServerErrorException < BaseException; end
8
+ end
@@ -0,0 +1,24 @@
1
+ module TophatterMerchant
2
+ class Image < Resource
3
+
4
+ attr_accessor :id, :fingerprint, :url
5
+
6
+ class << self
7
+
8
+ # request = RestClient::Request.new(method: :get, url: 'https://img0.etsystatic.com/101/0/7856452/il_fullxfull.882030160_r0tn.jpg')
9
+ # response = request.execute
10
+ # File.open('/tmp/foo.jpg', 'w') { |file| file.write(response.body) }
11
+ # ap TophatterMerchant::Image.create(File.open('/tmp/foo.jpg')).to_h
12
+ def create(file)
13
+ Image.new post(url: "#{path}.json", params: { data: file })
14
+ end
15
+
16
+ protected
17
+
18
+ def path
19
+ super + '/images'
20
+ end
21
+
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,76 @@
1
+ module TophatterMerchant
2
+ class Metadata < Resource
3
+ class << self
4
+
5
+ # ap TophatterMerchant::Metadata.all
6
+ def all
7
+ get(url: "#{path}.json")
8
+ end
9
+
10
+ # ap TophatterMerchant::Metadata.conditions
11
+ def conditions
12
+ get(url: "#{path}/conditions.json")
13
+ end
14
+
15
+ # ap TophatterMerchant::Metadata.categories
16
+ def categories
17
+ get(url: "#{path}/categories.json")
18
+ end
19
+
20
+ # ap TophatterMerchant::Metadata.sizes
21
+ def sizes
22
+ get(url: "#{path}/sizes.json")
23
+ end
24
+
25
+ # ap TophatterMerchant::Metadata.countries
26
+ def countries
27
+ get(url: "#{path}/countries.json")
28
+ end
29
+
30
+ # ap TophatterMerchant::Metadata.country_codes
31
+ def country_codes
32
+ get(url: "#{path}/country_codes.json")
33
+ end
34
+
35
+ # ap TophatterMerchant::Metadata.states
36
+ def states
37
+ get(url: "#{path}/states.json")
38
+ end
39
+
40
+ # ap TophatterMerchant::Metadata.provinces
41
+ def provinces
42
+ get(url: "#{path}/provinces.json")
43
+ end
44
+
45
+ # ap TophatterMerchant::Metadata.territories
46
+ def territories
47
+ get(url: "#{path}/territories.json")
48
+ end
49
+
50
+ # ap TophatterMerchant::Metadata.carriers
51
+ def carriers
52
+ get(url: "#{path}/carriers.json")
53
+ end
54
+
55
+ # ap TophatterMerchant::Metadata.brands
56
+ def brands
57
+ get(url: "#{path}/brands.json")
58
+ end
59
+
60
+ def materials
61
+ get(url: "#{path}/materials.json")
62
+ end
63
+
64
+ def gemstones
65
+ get(url: "#{path}/gemstones.json")
66
+ end
67
+
68
+ protected
69
+
70
+ def path
71
+ super + '/metadata'
72
+ end
73
+
74
+ end
75
+ end
76
+ end
@@ -0,0 +1,67 @@
1
+ module TophatterMerchant
2
+ class Order < Resource
3
+
4
+ attr_accessor :order_id, :status
5
+ attr_accessor :carrier, :tracking_number
6
+ attr_accessor :product_name, :product_identifier, :variation_identifier
7
+ attr_accessor :customer_name, :address1, :address2, :city, :state, :postal_code, :country
8
+ attr_accessor :available_refunds, :refund_amount
9
+ attr_accessor :disbursement_amount, :seller_fees_amount, :seller_fees
10
+ attr_accessor :paid_at, :created_at
11
+
12
+ class << self
13
+
14
+ # ap TophatterMerchant::Order.schema
15
+ def schema
16
+ get(url: "#{path}/schema.json")
17
+ end
18
+
19
+ # ap TophatterMerchant::Order.all.map(&:to_h)
20
+ # ap TophatterMerchant::Order.all(filter: 'unfulfilled').map(&:to_h)
21
+ # ap TophatterMerchant::Order.all(filter: 'fulfilled').map(&:to_h)
22
+ def all(filter: nil, page: 1, per_page: 50)
23
+ get(url: "#{path}.json", params: { filter: filter, page: page, per_page: per_page }).map do |hash|
24
+ Order.new(hash)
25
+ end
26
+ end
27
+
28
+ # ap TophatterMerchant::Order.retrieve(681195262).to_h
29
+ def retrieve(id)
30
+ Order.new get(url: "#{path}/retrieve.json", params: {
31
+ order_id: id
32
+ })
33
+ end
34
+
35
+ # ap TophatterMerchant::Order.fulfill(417953232, carrier: 'other', tracking_number: 'ABC123').to_h
36
+ def fulfill(id, carrier:, tracking_number:)
37
+ Order.new post(url: "#{path}/fulfill.json", params: {
38
+ order_id: id,
39
+ carrier: carrier,
40
+ tracking_number: tracking_number
41
+ })
42
+ end
43
+
44
+ def unfulfill(id)
45
+ post(url: "#{path}/#{id}/unfulfill.json")
46
+ end
47
+
48
+ # ap TophatterMerchant::Order.refund(417953232, type: 'full', reason: 'delay_in_shipping').to_h
49
+ # ap TophatterMerchant::Order.refund(417953232, type: 'partial', reason: 'other', fees: ['shipping_fee']).to_h
50
+ def refund(id, type:, reason:, fees: [])
51
+ Order.new post(url: "#{path}/refund.json", params: {
52
+ order_id: id,
53
+ type: type,
54
+ reason: reason,
55
+ fees: fees
56
+ })
57
+ end
58
+
59
+ protected
60
+
61
+ def path
62
+ super + '/orders'
63
+ end
64
+
65
+ end
66
+ end
67
+ end
@@ -0,0 +1,130 @@
1
+ module TophatterMerchant
2
+ class Product < Resource
3
+
4
+ attr_accessor :identifier
5
+ attr_accessor :category, :title, :description # Basics
6
+ attr_accessor :condition, :brand, :material # Facets
7
+ attr_accessor :available_quantity, :variations # Variations
8
+ attr_accessor :starting_bid, :buy_now_price, :retail_price, :cost_basis, :minimum_alerts_needed # Pricing
9
+ attr_accessor :shipping_origin, :shipping_price, :expedited_shipping_price, :days_to_fulfill, :days_to_deliver, :expedited_days_to_deliver, :weight # Shipping
10
+ attr_accessor :buy_one_get_one_price, :accessory_price, :accessory_description # Upsells
11
+ attr_accessor :primary_image, :extra_images, :all_images # Images
12
+ attr_accessor :slug, :ratings_average, :ratings_count # Ratings
13
+ attr_accessor :created_at, :updated_at, :disabled_at, :deleted_at # Timestamps
14
+ attr_accessor :blacklisted_at, :admin_hold_at, :slug, :internal_id # Other
15
+
16
+ def id
17
+ created_at.present? ? identifier : nil
18
+ end
19
+
20
+ # Available: thumbnail, square, medium, large, original.
21
+ def images(size: 'square')
22
+ if persisted?
23
+ all_images.collect { |image| image[size] }
24
+ else
25
+ ([primary_image] + extra_images.to_s.split('|')).compact
26
+ end
27
+ end
28
+
29
+ def copy
30
+ attributes = to_h
31
+
32
+ # Delete the attributes that shouldn't be copied.
33
+ %w(identifier primary_image extra_images all_images ratings_average ratings_count created_at updated_at disabled_at deleted_at blacklisted_at slug admin_hold_at).each do |attribute|
34
+ attributes.delete(attribute)
35
+ attributes['variations'].each { |variation| variation.delete(attribute) }
36
+ end
37
+
38
+ Product.new(attributes)
39
+ end
40
+
41
+ def to_param
42
+ slug || identifier
43
+ end
44
+
45
+ class << self
46
+
47
+ # ap TophatterMerchant::Product.schema
48
+ def schema
49
+ get(url: "#{path}/schema.json")
50
+ end
51
+
52
+ def search(query:, page: 1, per_page: 50, pagination: nil)
53
+ result = get(url: "#{path}/search.json", params: {
54
+ query: query,
55
+ page: page,
56
+ per_page: per_page,
57
+ pagination: pagination
58
+ })
59
+
60
+ if pagination.present?
61
+ result['results'] = result['results'].map { |hash| Product.new(hash) }
62
+ else
63
+ result.map { |hash| Product.new(hash) }
64
+ end
65
+
66
+ result
67
+ end
68
+
69
+ # ap TophatterMerchant::Product.all.map(&:to_h)
70
+ def all(status: nil, category: nil, page: 1, per_page: 50, pagination: nil, sort: nil)
71
+ result = get(url: "#{path}.json", params: {
72
+ status: status,
73
+ category: category,
74
+ page: page,
75
+ per_page: per_page,
76
+ pagination: pagination,
77
+ sort: sort
78
+ })
79
+
80
+ if pagination.present?
81
+ result['results'] = result['results'].map { |hash| Product.new(hash) }
82
+ else
83
+ result = result.map { |hash| Product.new(hash) }
84
+ end
85
+
86
+ result
87
+ end
88
+
89
+ def retrieve(id)
90
+ Product.new get(url: "#{path}/retrieve.json", params: { identifier: id })
91
+ end
92
+
93
+ # ap TophatterMerchant::Product.create(TophatterMerchant::Product.new({}).fill!.to_h).to_h
94
+ def create(data)
95
+ Product.new post(url: "#{path}.json", params: data)
96
+ end
97
+
98
+ # ap TophatterMerchant::Product.update('FOOBAR', buy_now_price: 11).to_h
99
+ def update(id, data)
100
+ Product.new post(url: "#{path}/update.json", params: data.merge(identifier: id))
101
+ end
102
+
103
+ # ap TophatterMerchant::Product.delete('FOOBAR').to_h
104
+ def delete(id)
105
+ Product.new post(url: "#{path}/delete.json", params: { identifier: id })
106
+ end
107
+
108
+ # ap TophatterMerchant::Product.disable('FOOBAR').to_h
109
+ def disable(id)
110
+ Product.new post(url: "#{path}/disable.json", params: { identifier: id })
111
+ end
112
+
113
+ # ap TophatterMerchant::Product.enable('FOOBAR').to_h
114
+ def enable(id)
115
+ Product.new post(url: "#{path}/enable.json", params: { identifier: id })
116
+ end
117
+
118
+ def upload(file, template:)
119
+ Product.new post(url: "#{path}/upload.json", params: { file: file, template: template })
120
+ end
121
+
122
+ protected
123
+
124
+ def path
125
+ super + '/products'
126
+ end
127
+
128
+ end
129
+ end
130
+ end
@@ -0,0 +1,110 @@
1
+ require 'active_model'
2
+
3
+ module TophatterMerchant
4
+ class Resource
5
+
6
+ include ActiveModel::Model
7
+
8
+ def self.attr_accessor(*vars)
9
+ attributes!(*vars)
10
+ super(*vars)
11
+ end
12
+
13
+ def initialize(hash)
14
+ self.attributes = hash
15
+ end
16
+
17
+ def attributes=(hash)
18
+ hash.each do |key, value|
19
+ if respond_to?(key)
20
+ self.class.attributes!(key)
21
+ send("#{key}=", value)
22
+ end
23
+ end
24
+ end
25
+
26
+ def to_h
27
+ self.class.attributes.keys.collect { |key| [key, send(key)] }.to_h
28
+ end
29
+
30
+ def persisted?
31
+ id.present?
32
+ end
33
+
34
+ private
35
+
36
+ def self.attributes
37
+ @attributes || {}
38
+ end
39
+
40
+ def self.attributes!(*vars)
41
+ @attributes ||= {}
42
+ vars.map(&:to_s).each { |var| @attributes[var] = true }
43
+ @attributes
44
+ end
45
+
46
+ class << self
47
+
48
+ protected
49
+
50
+ def get(url:, params: {})
51
+ execute request(method: :get, url: url, params: params)
52
+ end
53
+
54
+ def post(url:, params: {})
55
+ execute request(method: :post, url: url, params: params)
56
+ end
57
+
58
+ def put(url:, params: {})
59
+ execute request(method: :put, url: url, params: params)
60
+ end
61
+
62
+ def delete(url:, params: {})
63
+ execute request(method: :delete, url: url, params: params)
64
+ end
65
+
66
+ def execute(request)
67
+ begin
68
+ puts "#{request.method.upcase} #{request.url} #{request.payload.inspect}"
69
+ response = request.execute
70
+ raise BadContentTypeException.new, "The server didn't return JSON. You probably made a bad request." if response.headers[:content_type] == 'text/html; charset=utf-8'
71
+ JSON.parse(response.body)
72
+ rescue RestClient::Request::Unauthorized => e
73
+ raise UnauthorizedException.new, parse_error(e, e.message)
74
+ rescue RestClient::BadRequest => e
75
+ raise BadRequestException.new, parse_error(e, e.message)
76
+ rescue RestClient::ResourceNotFound => e
77
+ raise NotFoundException.new, parse_error(e, 'The API path you requested does not exist.')
78
+ rescue RestClient::InternalServerError => e
79
+ raise ServerErrorException.new, parse_error(e, 'The server encountered an internal error. This is probably a bug, and you should contact support.')
80
+ end
81
+ end
82
+
83
+ def request(method:, url:, params:)
84
+ payload = if TophatterMerchant.access_token.present?
85
+ params.merge(access_token: TophatterMerchant.access_token)
86
+ else
87
+ params
88
+ end
89
+
90
+ RestClient::Request.new(method: method, url: url, payload: payload, accept: :json)
91
+ end
92
+
93
+ def path
94
+ TophatterMerchant.api_path
95
+ end
96
+
97
+ private
98
+
99
+ def parse_error(exception, fallback)
100
+ error = begin
101
+ JSON.parse(exception.response)
102
+ rescue
103
+ {}
104
+ end
105
+ raise BadRequestException.new, error['message'] || fallback
106
+ end
107
+
108
+ end
109
+ end
110
+ end
@@ -0,0 +1,41 @@
1
+ module TophatterMerchant
2
+ class Variation < Resource
3
+
4
+ attr_accessor :identifier, :size, :color, :quantity, :created_at
5
+
6
+ def id
7
+ created_at.present? ? identifier : nil
8
+ end
9
+
10
+ class << self
11
+
12
+ # ap TophatterMerchant::Variation.schema
13
+ def schema
14
+ get(url: "#{path}/schema.json")
15
+ end
16
+
17
+ # ap TophatterMerchant::Variation.retrieve('FOOBAR-R').to_h
18
+ def retrieve(identifier)
19
+ Variation.new get(url: "#{path}/retrieve.json", params: { identifier: identifier })
20
+ end
21
+
22
+ # ap Variation.create(product_identifier: '6631A', identifier: '6631A-GRAY', color: 'Gray', quantity: 33).to_h
23
+ def create(params)
24
+ Variation.new post(url: "#{path}.json", params: params)
25
+ end
26
+
27
+ # ap TophatterMerchant::Variation.update('FOOBAR-R', quantity: 100).to_h
28
+ def update(identifier, data)
29
+ Variation.new post(url: "#{path}/update.json", params: data.merge(identifier: identifier))
30
+ end
31
+
32
+ protected
33
+
34
+ def path
35
+ super + '/variations'
36
+ end
37
+
38
+ end
39
+
40
+ end
41
+ end
@@ -0,0 +1,3 @@
1
+ module TophatterMerchant
2
+ VERSION = '1.0'
3
+ end
@@ -0,0 +1,24 @@
1
+ require 'rest-client'
2
+ require 'tophatter_merchant'
3
+ require 'webmock/rspec'
4
+
5
+ WebMock.disable_net_connect!(allow_localhost: true)
6
+
7
+ # http://rubydoc.info/gems/rspec-core/RSpec/Core/Configuration
8
+ RSpec.configure do |config|
9
+
10
+ config.expect_with :rspec do |expectations|
11
+ expectations.include_chain_clauses_in_custom_matcher_descriptions = true
12
+ end
13
+
14
+ config.mock_with :rspec do |mocks|
15
+ mocks.verify_partial_doubles = true
16
+ end
17
+
18
+ config.before(:each) do
19
+ stub_request(:get, /.*metadata\.json/).to_return(File.new(File.dirname(__FILE__) + '/../tmp/stubs/metadata.json'))
20
+ stub_request(:get, /.*conditions\.json/).to_return(File.new(File.dirname(__FILE__) + '/../tmp/stubs/metadata/conditions.json'))
21
+ stub_request(:get, /.*gemstones\.json/).to_return(File.new(File.dirname(__FILE__) + '/../tmp/stubs/metadata/gemstones.json'))
22
+ end
23
+
24
+ end
@@ -0,0 +1,38 @@
1
+ require 'spec_helper'
2
+
3
+ # rspec spec/tophatter_merchant/metadata_spec.rb
4
+ describe TophatterMerchant::Metadata do
5
+
6
+ it 'returns metadata' do
7
+ metadata = TophatterMerchant::Metadata.all
8
+ expect(metadata.keys.include?('categories')).to be true
9
+ expect(metadata.keys.include?('conditions')).to be true
10
+ expect(metadata.keys.include?('sizes')).to be true
11
+ expect(metadata.keys.include?('colors')).to be true
12
+ expect(metadata.keys.include?('countries')).to be true
13
+ expect(metadata.keys.include?('states')).to be true
14
+ expect(metadata.keys.include?('provinces')).to be true
15
+ expect(metadata.keys.include?('territories')).to be true
16
+ expect(metadata.keys.include?('carriers')).to be true
17
+ expect(metadata.keys.include?('brands')).to be true
18
+ expect(metadata.keys.include?('materials')).to be true
19
+ expect(metadata.keys.include?('gemstones')).to be true
20
+ end
21
+
22
+ it 'returns conditions' do
23
+ conditions = TophatterMerchant::Metadata.conditions
24
+ expect(conditions.include?('New')).to be true
25
+ expect(conditions.include?('New with Tags')).to be true
26
+ expect(conditions.include?('New with Defects')).to be true
27
+ expect(conditions.include?('Used')).to be true
28
+ expect(conditions.include?('Refurbished')).to be true
29
+ end
30
+
31
+ it 'returns gemstones' do
32
+ gemstones = TophatterMerchant::Metadata.gemstones
33
+ expect(gemstones['Gemstones'].include?('Diamond')).to be true
34
+ expect(gemstones['Gemstones'].include?('Ruby')).to be true
35
+ expect(gemstones['Gemstones'].include?("Micheal Jackson's Hair Diamond")).to be false
36
+ end
37
+
38
+ end
@@ -0,0 +1,11 @@
1
+ require 'spec_helper'
2
+
3
+ # rspec spec/tophatter_merchant/product_spec.rb
4
+ describe TophatterMerchant::Product do
5
+
6
+ it 'instantiates' do
7
+ product = TophatterMerchant::Product.new({})
8
+ expect(product.persisted?).to be false
9
+ end
10
+
11
+ end
@@ -0,0 +1,12 @@
1
+ HTTP/1.1 200 OK
2
+ X-Frame-Options: SAMEORIGIN
3
+ X-XSS-Protection: 1; mode=block
4
+ X-Content-Type-Options: nosniff
5
+ Content-Type: application/json; charset=utf-8
6
+ ETag: W/"45b19639776dd1683511d9f9d258665d"
7
+ Cache-Control: max-age=0, private, must-revalidate
8
+ X-Request-Id: 75fb5e61-0b1a-480d-b9c0-1708968dd02a
9
+ X-Runtime: 0.103416
10
+ Transfer-Encoding: chunked
11
+
12
+ {"categories":["Accessories | Bags | Clutches & Evening Bags","Accessories | Bags | Crossbody","Accessories | Bags | Satchels","Accessories | Bags | Shoulder Bags & Hobos","Accessories | Bags | Totes & Backpacks","Accessories | Bags | Weekenders & Duffles","Accessories | Bags | Other","Accessories | Sunglasses","Accessories | Watches","Accessories | Wallets","Accessories | Wristlets","Accessories | Other Accessories | Scarves","Accessories | Other Accessories | Keychains","Accessories | Other Accessories | Hats","Accessories | Other Accessories | Hair Accessories","Accessories | Other Accessories | Electronic Accessories","Accessories | Other Accessories | Belts","Accessories | Other Accessories | Ties","Accessories | Other Accessories | Tights/Socks","Accessories | Other Accessories | Other","Apparel | Intimates | Bras","Apparel | Intimates | Underwear","Apparel | Intimates | Shapewear","Apparel | Intimates | Other","Apparel | Tops | T-Shirts","Apparel | Tops | Blouses","Apparel | Tops | Shirts","Apparel | Tops | Sweaters","Apparel | Tops | Sweatshirts","Apparel | Tops | Vests","Apparel | Tops | Tanks","Apparel | Tops | Other","Apparel | Jackets/Outerwear | Jackets","Apparel | Jackets/Outerwear | Coats","Apparel | Jackets/Outerwear | Other","Apparel | Bottoms | Shorts","Apparel | Bottoms | Skirts","Apparel | Bottoms | Pants","Apparel | Bottoms | Leggings","Apparel | Bottoms | Other","Apparel | Dresses","Apparel | Sports & Fitness","Apparel | Swim | Tops","Apparel | Swim | Bottoms","Apparel | Swim | One-pieces","Apparel | Swim | Sets (Top & Bottom)","Apparel | Swim | Coverups","Apparel | Other","Beauty | Skincare | Cleansers","Beauty | Skincare | Moisturizer","Beauty | Skincare | Other","Beauty | Haircare | Shampoo","Beauty | Haircare | Conditioner","Beauty | Haircare | Styling Tools","Beauty | Haircare | Other","Beauty | Hair Tools | Flat Iron","Beauty | Hair Tools | Blow Dryer","Beauty | Hair Tools | Curling Iron","Beauty | Hair Tools | Brushes","Beauty | Hair Tools | Other","Beauty | Fragrance","Beauty | Makeup (Cosmetics) | Lip","Beauty | Makeup (Cosmetics) | Eye","Beauty | Makeup (Cosmetics) | Face","Beauty | Makeup (Cosmetics) | Cheek","Beauty | Makeup (Cosmetics) | Other","Beauty | Nails | Nail Polish","Beauty | Nails | Nail Art","Beauty | Nails | Manicure Set","Beauty | Nails | Nail Tools & Files","Beauty | Nails | Other","Beauty | Brushes | Face","Beauty | Brushes | Eyes","Beauty | Brushes | Lip","Beauty | Brushes | Sets","Beauty | Brushes | Other","Beauty | Bath & Body","Beauty | Other","Collectibles | Memorabilia","Collectibles | Coins","Collectibles | Other","Edibles | Healthy","Edibles | Desserts","Edibles | Gourmet","Edibles | Non Alcoholic Beverages","Edibles | Other","Electronics | Audio | Headphones","Electronics | Audio | Home Theater Systems","Electronics | Audio | Turntables","Electronics | Audio | MP3 Players","Electronics | Audio | Shelf Systems","Electronics | Audio | Speakers","Electronics | Audio | Other","Electronics | Hardware (Computers/Tablets/Phones) | TV","Electronics | Hardware (Computers/Tablets/Phones) | Audio","Electronics | Hardware (Computers/Tablets/Phones) | Mobile","Electronics | Hardware (Computers/Tablets/Phones) | Video","Electronics | Hardware (Computers/Tablets/Phones) | Computers/Tablets","Electronics | Hardware (Computers/Tablets/Phones) | Printers","Electronics | Hardware (Computers/Tablets/Phones) | Other","Electronics | TV & Projectors","Electronics | Photography/Camera","Electronics | Gaming","Electronics | Accessories","Electronics | Other","Home | Bed | Pillows","Home | Bed | Sheets","Home | Bed | Comforter/Duvet","Home | Bed | Throw","Home | Bed | Other","Home | Bath | Towels","Home | Bath | Other","Home | Kitchen & Dining | Dinnerware","Home | Kitchen & Dining | Flatware","Home | Kitchen & Dining | Serveware","Home | Kitchen & Dining | Bar & Glassware","Home | Kitchen & Dining | Linens","Home | Kitchen & Dining | Other","Home | Lighting","Home | Furniture","Home | Storage & Organization","Home | Electrics & Appliances | Kitchen","Home | Electrics & Appliances | Cleaning","Home | Electrics & Appliances | Other","Home | Decor | Art","Home | Decor | Wall Decor & Mirrors","Home | Decor | Vases","Home | Decor | Other","Home | Outdoor | Tool","Home | Outdoor | Seeds","Home | Outdoor | Outdoor Furniture","Home | Outdoor | Outdoor Accessories","Home | Outdoor | Other","Home | Pet Supplies","Home | Other","Jewelry | Rings","Jewelry | Necklaces","Jewelry | Bracelets","Jewelry | Earrings","Jewelry | Other","Jewelry | Sets","Kids | Baby Apparel | Jackets/Outerwear","Kids | Baby Apparel | Tops","Kids | Baby Apparel | Bottoms","Kids | Baby Apparel | Dresses","Kids | Baby Apparel | Onesies","Kids | Baby Apparel | Pajamas","Kids | Baby Apparel | Sets","Kids | Baby Apparel | Other","Kids | Toddler Apparel | Jackets/Outerwear","Kids | Toddler Apparel | Tops","Kids | Toddler Apparel | Bottoms","Kids | Toddler Apparel | Dresses","Kids | Toddler Apparel | Onesies","Kids | Toddler Apparel | Pajamas","Kids | Toddler Apparel | Sets","Kids | Toddler Apparel | Other","Kids | Kids/Youth Apparel | Jackets/Outerwear","Kids | Kids/Youth Apparel | Tops","Kids | Kids/Youth Apparel | Bottoms","Kids | Kids/Youth Apparel | Dresses","Kids | Kids/Youth Apparel | Onesies","Kids | Kids/Youth Apparel | Pajamas","Kids | Kids/Youth Apparel | Sets","Kids | Kids/Youth Apparel | Other","Kids | Baby Shoes | Boots","Kids | Baby Shoes | Sneakers","Kids | Baby Shoes | Sandals","Kids | Baby Shoes | Booties","Kids | Baby Shoes | Other","Kids | Toddler Shoes | Boots","Kids | Toddler Shoes | Sneakers","Kids | Toddler Shoes | Sandals","Kids | Toddler Shoes | Booties","Kids | Toddler Shoes | Other","Kids | Kids/Youth Shoes | Boots","Kids | Kids/Youth Shoes | Sneakers","Kids | Kids/Youth Shoes | Sandals","Kids | Kids/Youth Shoes | Booties","Kids | Kids/Youth Shoes | Other","Kids | Games & Toys","Shoes | Flats","Shoes | Heels","Shoes | Boots","Shoes | Sneakers","Shoes | Sandals","Shoes | Wedges","Shoes | Other","Supplies | Jewelry Supplies | Beads","Supplies | Jewelry Supplies | Charms","Supplies | Jewelry Supplies | Pins","Supplies | Jewelry Supplies | Gemstones","Supplies | Jewelry Supplies | Accessories","Supplies | Jewelry Supplies | Tools","Supplies | Jewelry Supplies | Findings","Supplies | Jewelry Supplies | Other","Supplies | Craft Supplies | Kits","Supplies | Craft Supplies | Fabric","Supplies | Craft Supplies | Accessories","Supplies | Craft Supplies | Tools","Supplies | Craft Supplies | Other","Supplies | Other","Other | Other"],"conditions":["New","New with Tags","New with Defects","Used - Excellent","Used - Good","Used - Fair","Refurbished - Manufacturer","Refurbished - Seller"],"sizes":{"Accessories":["Small","Medium","Large","Extra Large"],"Apparel":["XS","S","M","L","XL","XXL","XXXL","1x","2x","3x","4x","0","2","4","6","8","10","12","14","16","18","20","22","24","26","28","30"],"Beauty":["Full","Travel","Deluxe Sample","Sample","Tester","Palette","Set","Mini"],"Shoes":["5","5.5","6","6.5","7","7.5","8","8.5","9","9.5","10","10.5","11","11.5","12"],"Rings":["4","4.5","5","5.5","6","6.5","7","7.5","8","8.5","9","9.5","10","10.5","11","11.5","12","12.5","13","13.5","14"],"Necklaces":["Extra Short (<16\")","Short (16\"-20\")","Medium (20\"-24\")","Long (24\"-28\")","Extra Long(28\"+)"],"Baby Apparel":["Preemie (P)","Newborn (NB)","0/3","3M","3/6","6M","6/9","9M","12M","18M","24M"],"Toddler Apparel":["2T","3T","4T","5T"],"Kids/Youth Apparel":["4","5","6","6X","7","8","10","12","14","16","18","XS","S","M","L","XL"],"Baby Shoes":["0","0.5","1","1.5","2","2.5","3","3.5","4","4.5","5","5.5","6","6.5","7"],"Toddler Shoes":["7.5","8","8.5","9","9.5","10","10.5","11","11.5","12"],"Kids/Youth Shoes":["12.5","13","13.5","1Y","1.5Y","2Y","2.5Y","3Y","3.5Y","4Y","4.5Y","5Y","5.5Y","6Y","6.5Y","7Y"]},"colors":["Red","Orange","Yellow","Green","Blue","Purple","Black","White","Gray","Pink","Brown","Silver","Gold","Clear"],"countries":["United States","Canada","India","China","Hong Kong","Afghanistan","Aland Islands","Albania","Algeria","American Samoa","Andorra","Angola","Anguilla","Antarctica","Antigua and Barbuda","Argentina","Armenia","Aruba","Australia","Austria","Azerbaijan","Bahamas","Bahrain","Bangladesh","Barbados","Belarus","Belgium","Belize","Benin","Bermuda","Bhutan","Bolivia","Bonaire, Sint Eustatius and Saba","Bosnia and Herzegovina","Botswana","Bouvet Island","Brazil","British Indian Ocean Territory","Brunei Darussalam","Bulgaria","Burkina Faso","Burundi","Cambodia","Cameroon","Cape Verde","Cayman Islands","Central African Republic","Chad","Chile","Christmas Island","Cocos (Keeling) Islands","Colombia","Comoros","Congo","Congo, The Democratic Republic Of The","Cook Islands","Costa Rica","Croatia","Cuba","Curaçao","Cyprus","Czech Republic","Côte D'Ivoire","Denmark","Djibouti","Dominica","Dominican Republic","Ecuador","Egypt","El Salvador","Equatorial Guinea","Eritrea","Estonia","Ethiopia","Falkland Islands (Malvinas)","Faroe Islands","Fiji","Finland","France","French Guiana","French Polynesia","French Southern Territories","Gabon","Gambia","Georgia","Germany","Ghana","Gibraltar","Greece","Greenland","Grenada","Guadeloupe","Guam","Guatemala","Guernsey","Guinea","Guinea-Bissau","Guyana","Haiti","Heard and McDonald Islands","Holy See (Vatican City State)","Honduras","Hungary","Iceland","Indonesia","Iran, Islamic Republic Of","Iraq","Ireland","Isle of Man","Israel","Italy","Jamaica","Japan","Jersey","Jordan","Kazakhstan","Kenya","Kiribati","Korea, Democratic People's Republic Of","Korea, Republic of","Kuwait","Kyrgyzstan","Lao People's Democratic Republic","Latvia","Lebanon","Lesotho","Liberia","Libya","Liechtenstein","Lithuania","Luxembourg","Macao","Macedonia, the Former Yugoslav Republic Of","Madagascar","Malawi","Malaysia","Maldives","Mali","Malta","Marshall Islands","Martinique","Mauritania","Mauritius","Mayotte","Mexico","Micronesia, Federated States Of","Moldova, Republic of","Monaco","Mongolia","Montenegro","Montserrat","Morocco","Mozambique","Myanmar","Namibia","Nauru","Nepal","Netherlands","Netherlands Antilles","New Caledonia","New Zealand","Nicaragua","Niger","Nigeria","Niue","Norfolk Island","Northern Mariana Islands","Norway","Oman","Pakistan","Palau","Palestinian Territory, Occupied","Panama","Papua New Guinea","Paraguay","Peru","Philippines","Pitcairn","Poland","Portugal","Puerto Rico","Qatar","Romania","Russian Federation","Rwanda","Réunion","Saint Barthélemy","Saint Helena","Saint Kitts And Nevis","Saint Lucia","Saint Martin","Saint Pierre And Miquelon","Saint Vincent And The Grenedines","Samoa","San Marino","Sao Tome and Principe","Saudi Arabia","Senegal","Serbia","Seychelles","Sierra Leone","Singapore","Sint Maarten","Slovakia","Slovenia","Solomon Islands","Somalia","South Africa","South Georgia and the South Sandwich Islands","South Sudan","Spain","Sri Lanka","Sudan","Suriname","Svalbard And Jan Mayen","Swaziland","Sweden","Switzerland","Syrian Arab Republic","Taiwan, Republic Of China","Tajikistan","Tanzania, United Republic of","Thailand","Timor-Leste","Togo","Tokelau","Tonga","Trinidad and Tobago","Tunisia","Turkey","Turkmenistan","Turks and Caicos Islands","Tuvalu","Uganda","Ukraine","United Arab Emirates","United Kingdom","United States Minor Outlying Islands","Uruguay","Uzbekistan","Vanuatu","Venezuela, Bolivarian Republic of","Vietnam","Virgin Islands, British","Virgin Islands, U.S.","Wallis and Futuna","Western Sahara","Yemen","Zambia","Zimbabwe"],"country_codes":{"United States":"USA","Canada":"CAN","India":"IND","China":"CHN","Hong Kong":"HKG","Andorra":"AND","United Arab Emirates":"ARE","Afghanistan":"AFG","Antigua and Barbuda":"ATG","Anguilla":"AIA","Albania":"ALB","Armenia":"ARM","Netherlands Antilles":"ANT","Angola":"AGO","Antarctica":"ATA","Argentina":"ARG","American Samoa":"ASM","Austria":"AUT","Australia":"AUS","Aruba":"ABW","Aland Islands":"ALA","Azerbaijan":"AZE","Bosnia and Herzegovina":"BIH","Barbados":"BRB","Bangladesh":"BGD","Belgium":"BEL","Burkina Faso":"BFA","Bulgaria":"BGR","Bahrain":"BHR","Burundi":"BDI","Benin":"BEN","Saint Barthélemy":"BLM","Bermuda":"BMU","Brunei Darussalam":"BRN","Bolivia":"BOL","Bonaire, Sint Eustatius and Saba":"BES","Brazil":"BRA","Bahamas":"BHS","Bhutan":"BTN","Bouvet Island":"BVT","Botswana":"BWA","Belarus":"BLR","Belize":"BLZ","Cocos (Keeling) Islands":"CCK","Congo, The Democratic Republic Of The":"COD","Central African Republic":"CAF","Congo":"COG","Switzerland":"CHE","Côte D'Ivoire":"CIV","Cook Islands":"COK","Chile":"CHL","Cameroon":"CMR","Colombia":"COL","Costa Rica":"CRI","Cuba":"CUB","Cape Verde":"CPV","Curaçao":"CUW","Christmas Island":"CXR","Cyprus":"CYP","Czech Republic":"CZE","Germany":"DEU","Djibouti":"DJI","Denmark":"DNK","Dominica":"DMA","Dominican Republic":"DOM","Algeria":"DZA","Ecuador":"ECU","Estonia":"EST","Egypt":"EGY","Western Sahara":"ESH","Eritrea":"ERI","Spain":"ESP","Ethiopia":"ETH","Finland":"FIN","Fiji":"FJI","Falkland Islands (Malvinas)":"FLK","Micronesia, Federated States Of":"FSM","Faroe Islands":"FRO","France":"FRA","Gabon":"GAB","Grenada":"GRD","Georgia":"GEO","French Guiana":"GUF","Guernsey":"GGY","Ghana":"GHA","Gibraltar":"GIB","Greenland":"GRL","Gambia":"GMB","Guinea":"GIN","Guadeloupe":"GLP","Equatorial Guinea":"GNQ","Greece":"GRC","South Georgia and the South Sandwich Islands":"SGS","Guatemala":"GTM","Guam":"GUM","Guinea-Bissau":"GNB","Guyana":"GUY","Heard and McDonald Islands":"HMD","Honduras":"HND","Croatia":"HRV","Haiti":"HTI","Hungary":"HUN","Indonesia":"IDN","Ireland":"IRL","Israel":"ISR","Isle of Man":"IMN","British Indian Ocean Territory":"IOT","Iraq":"IRQ","Iran, Islamic Republic Of":"IRN","Iceland":"ISL","Italy":"ITA","Jersey":"JEY","Jamaica":"JAM","Jordan":"JOR","Japan":"JPN","Kenya":"KEN","Kyrgyzstan":"KGZ","Cambodia":"KHM","Kiribati":"KIR","Comoros":"COM","Saint Kitts And Nevis":"KNA","Korea, Democratic People's Republic Of":"PRK","Korea, Republic of":"KOR","Kuwait":"KWT","Cayman Islands":"CYM","Kazakhstan":"KAZ","Lao People's Democratic Republic":"LAO","Lebanon":"LBN","Saint Lucia":"LCA","Liechtenstein":"LIE","Sri Lanka":"LKA","Liberia":"LBR","Lesotho":"LSO","Lithuania":"LTU","Luxembourg":"LUX","Latvia":"LVA","Libya":"LBY","Morocco":"MAR","Monaco":"MCO","Moldova, Republic of":"MDA","Montenegro":"MNE","Saint Martin":"MAF","Madagascar":"MDG","Marshall Islands":"MHL","Macedonia, the Former Yugoslav Republic Of":"MKD","Mali":"MLI","Myanmar":"MMR","Mongolia":"MNG","Macao":"MAC","Northern Mariana Islands":"MNP","Martinique":"MTQ","Mauritania":"MRT","Montserrat":"MSR","Malta":"MLT","Mauritius":"MUS","Maldives":"MDV","Malawi":"MWI","Mexico":"MEX","Malaysia":"MYS","Mozambique":"MOZ","Namibia":"NAM","New Caledonia":"NCL","Niger":"NER","Norfolk Island":"NFK","Nigeria":"NGA","Nicaragua":"NIC","Netherlands":"NLD","Norway":"NOR","Nepal":"NPL","Nauru":"NRU","Niue":"NIU","New Zealand":"NZL","Oman":"OMN","Panama":"PAN","Peru":"PER","French Polynesia":"PYF","Papua New Guinea":"PNG","Philippines":"PHL","Pakistan":"PAK","Poland":"POL","Saint Pierre And Miquelon":"SPM","Pitcairn":"PCN","Puerto Rico":"PRI","Palestinian Territory, Occupied":"PSE","Portugal":"PRT","Palau":"PLW","Paraguay":"PRY","Qatar":"QAT","Réunion":"REU","Romania":"ROU","Serbia":"SRB","Russian Federation":"RUS","Rwanda":"RWA","Saudi Arabia":"SAU","Solomon Islands":"SLB","Seychelles":"SYC","Sudan":"SDN","Sweden":"SWE","Singapore":"SGP","Saint Helena":"SHN","Slovenia":"SVN","Svalbard And Jan Mayen":"SJM","Slovakia":"SVK","Sierra Leone":"SLE","San Marino":"SMR","Senegal":"SEN","Somalia":"SOM","Suriname":"SUR","South Sudan":"SSD","Sao Tome and Principe":"STP","El Salvador":"SLV","Sint Maarten":"SXM","Syrian Arab Republic":"SYR","Swaziland":"SWZ","Turks and Caicos Islands":"TCA","Chad":"TCD","French Southern Territories":"ATF","Togo":"TGO","Thailand":"THA","Tajikistan":"TJK","Tokelau":"TKL","Timor-Leste":"TLS","Turkmenistan":"TKM","Tunisia":"TUN","Tonga":"TON","Turkey":"TUR","Trinidad and Tobago":"TTO","Tuvalu":"TUV","Taiwan, Republic Of China":"TWN","Tanzania, United Republic of":"TZA","Ukraine":"UKR","Uganda":"UGA","United Kingdom":"GBR","United States Minor Outlying Islands":"UMI","Uruguay":"URY","Uzbekistan":"UZB","Holy See (Vatican City State)":"VAT","Saint Vincent And The Grenedines":"VCT","Venezuela, Bolivarian Republic of":"VEN","Virgin Islands, British":"VGB","Virgin Islands, U.S.":"VIR","Vietnam":"VNM","Vanuatu":"VUT","Wallis and Futuna":"WLF","Samoa":"WSM","Yemen":"YEM","Mayotte":"MYT","South Africa":"ZAF","Zambia":"ZMB","Zimbabwe":"ZWE"},"states":{"Alaska":"AK","Alabama":"AL","Arkansas":"AR","American Samoa":"AS","Arizona":"AZ","California":"CA","Colorado":"CO","Connecticut":"CT","District of Columbia":"DC","Delaware":"DE","Florida":"FL","Georgia":"GA","Guam":"GU","Hawaii":"HI","Iowa":"IA","Idaho":"ID","Illinois":"IL","Indiana":"IN","Kansas":"KS","Kentucky":"KY","Louisiana":"LA","Massachusetts":"MA","Maryland":"MD","Maine":"ME","Michigan":"MI","Minnesota":"MN","Missouri":"MO","Northern Mariana Islands":"MP","Mississippi":"MS","Montana":"MT","North Carolina":"NC","North Dakota":"ND","Nebraska":"NE","New Hampshire":"NH","New Jersey":"NJ","New Mexico":"NM","Nevada":"NV","New York":"NY","Ohio":"OH","Oklahoma":"OK","Oregon":"OR","Pennsylvania":"PA","Puerto Rico":"PR","Rhode Island":"RI","South Carolina":"SC","South Dakota":"SD","Tennessee":"TN","Texas":"TX","United States Minor Outlying Islands":"UM","Utah":"UT","Virginia":"VA","Virgin Islands":"VI","Vermont":"VT","Washington":"WA","Wisconsin":"WI","West Virginia":"WV","Wyoming":"WY","Armed Forces Americas (except Canada)":"AA","Armed Forces Africa, Canada, Europe, Middle East":"AE","Armed Forces Pacific":"AP"},"provinces":{"Alberta":"AB","British Columbia":"BC","Manitoba":"MB","New Brunswick":"NB","Newfoundland and Labrador":"NL","Nova Scotia":"NS","Northwest Territories":"NT","Nunavut":"NU","Ontario":"ON","Prince Edward Island":"PE","Quebec":"QC","Saskatchewan":"SK","Yukon Territory":"YT"},"territories":{"Australian Capital Territory":"ACT","New South Wales":"NSW","Northern Territory":"NT","Queensland":"QLD","South Australia":"SA","Tasmania":"TAS","Victoria":"VIC","Western Australia":"WA"},"carriers":{"USPS":"usps","FedEx":"fedex","Canada Post":"canada-post","UPS":"ups","UPS Mail Innovations":"ups-mi","DHL":"dhl","DHL Global Mail":"dhl-global-mail","OnTrac":"ontrac","China Post":"china-post","China EMS":"china-ems","Yanwen":"yanwen","India Post":"india-post","Asendia USA":"asendia-usa","Hong Kong Post":"hong-kong-post","DHL Hong Kong":"dhl-hk","Japan Post":"japan-post","Taiwan Post":"taiwan-post","Singapore Post":"singapore-post","DHL Global Mail Asia":"dhl-global-mail-asia","EMPS Express":"empsexpess","German Post DHL":"dhl-germany","Russian Post":"russian-post","Malaysia Post EMS / Poslaju":"malaysia-post","Thailand Thai Post":"thailand-post","Vietnam Post":"vnpost","Cambodia Post":"cambodia-post","Netherlands Post":"postnl","Turkey Post":"ptt-posta","Yun Express":"yunexpress","German Post":"deutsch-post","Irish Post":"an-post","Ukraine Post":"ukrposhta","Lasership":"lasership","Korea Post":"korea-post","DHL Global Forwarding":"dhl-global-forwarding","Other":"other"},"brands":{"Accessories":["7 For All Mankind","AX Paris","Adidas","Adrienne Vittadini","Aeropostale","Ann Taylor","Anne Klein","Anne Michelle","Apt. 9","Armani","Ash","B Makowsky","BCBG","Baby phat","Bally","Bally White","Banana Republic","Bebe","Betsey Johnson","Bindya","Bottega Veneta","Brahmin","Brandy Melville","Brighton","Burberry","Burgi","Calvin Klein","Carlos Falchi","Carrera","Cartier","Celine","Chanel","Chico's","Chloe","Christian Dior","Coach","Cole Haan","Converse","Courrèges","DKNY","Diesel","Dior","Dockers","Dolce & Gabbana","Donna Karan","Dooney & Bourke","Ecko","Ed Hardy","Elie Tahari","Elizabeth Arden","Elle","Ellen Tracy","Emanuel Ungaro","Emilio Pucci","Escada","Etienne Aigner","Etro","Etro Milano","Expressions NYC","Fantas-Eyes","Fendi","Ferrari","Fila","Fossil","Francesco Biasia","Franco Sarto","French Connection","Frye","Furla","Giorgio Armani","Gucci","Guess","Harley Davidson","Invicta","Isabella Fiore","J.Crew","Jessica Simpson","Jimmy Choo","Joan & David","Juicy Couture","Karl Lagerfeld","Kate Spade","Kathy Van Zeeland","Kay Unger","Kenneth Cole","Kenzo","Kipling","Lacoste","Lambertson Truex","Lancel","Lands' End","Larry Levine","LeSportsac","Liz Claiborne","London Fog","Longchamp","Louis Vuitton","Lucky Brand","Lululemon","Marc Jacobs","Mario Valentino","Matt & Nat","Michael Kors","Michael by Michael Kors","Miu Miu","Moschino","Nicole Miller","Nike","Nina Ricci","Nine West","NorthFace","Olivia + Joy","Oscar De La Renta","Patricia Nash","Paul Smith","Pinky & Dianne","Prada","RAY-BAN","Ralph Lauren","Rebecca Minkoff","Rena Lange","Roberto Cavalli","Rock & Republic","Rootote","Saks Fifth Avenue","Salvatore Ferragamo","Sam Edelman","Samantha Savasa","Sergio Rossi","Skagen","Sonia Rykiel","Sonoma","Steve Madden","Style & Co.","Susan Gail","Talbots","Ted Baker","Timberland","Tod's","Tommy Hilfiger","Tory Burch","Travelon","UGG","Valentino","Vans","Vera Bradley","Vera Wang","Versace","Versace Collection","Victoria's Secret","Vince Camuto"],"Beauty":["Adidas","Amazing Cosmetics","Amor","Amouage","Anastasia","Anne Klein","Aquolina","Aramis","Ariana Grande","Armani","Azzaro","Bahama","Bare Escentuals","Bebe","Ben Nye","Benefit","Beyonce","Billion Dollar Brow","Bobbi Brown","Boucheron","Britney Spears","Bumble and Bumble","Burberry","Bvlgari","CURVE","Cacharel","Calvin Klein","Cargo","Carolina Herrera","Cartier","Chanel","Chloe","Christian Audigier","Christian Dior","Clarisonic","Clinique","Coach","Coco Chanel","Coquillete","Creed","D&G","Davidoff","Diesel","Dior","Dolce & Gabbana","Donna Karan","Ed Hardy","Elizabeth Arden","Elizabeth Taylor","Escada","Essie","Estee Lauder","Ferrari","Fresh","Geir Ness","Giorgio Armani","Givenchy","Gucci","Guerlain","Guess","Gwen Stefani","Hampton Sun","Hourglass","Hugo Boss","Hydroxatone","Infinique","Isaac Mizrahi","Iso","Issey Miyake","Jean Paul","Jennifer Lopez","Jessica Simpson","Jesus Del Pozo","Jimmy Choo","John Varvatos","Joop","Juicy Couture","Justin Bieber","Kat Von D","Katy Perry","Kenneth Cole","Kim Kardashian","Korres","LORAC","Lacoste","Lady Gaga","Lalique","Laura Geller","Laura Mercier","LiLash","Lime Crime","Liz Claiborne","Lolita Lempicka","Loreal","Loris Azzaro","Lucky","MAC","Makeup Forever","Marc Jacobs","Mary Kay","Michael Jordan","Michael Kors","Michel Germain","NARS Cosmetics","NAUTICA","Narciso Rodriguez","Neiman Marcus","Obsessive Compulsive Cosmetics","Oscar de la Renta","Pacifica","Paco Rabanne","Penhaligon's Blenheim Bouquet","Perry Ellis","Philosophy","Pop Beauty","Ralph Lauren","Revlon","Rihanna","Roberto Cavalli","Saks Fifth Avenue","Sephora","Shiseido","Smashbox","Stella McCartney","Stila","StriVectin","Studio Gear","Tarte","Taylor Swift","The Balm","Thierry Mugler","TokyoMilk","Tommy Hilfiger","Too Faced","True Religion","Ulta","Urban Decay","Usher","Vera Wang","Versace","Victoria's Secret","Viktor & Rolf","Vince Camuto","Yves Saint Laurent","bareMinerals"],"Electronics":["Acer","Alcatel","Amazon","Apple","Asus","Barnes & Noble","Beats By Dre","Belkin","Bose","Canon","Chromo Inc","Dell","Dyson","Fujifilm","GE","Google","Griffin","HDE","HP","Harman Kardon ","Husky","Incipio","Insignia","JBL","Koramzi","LG","Lenovo","Logitech","Microsoft","Nextbook","Nikon","Nokia","NutriBullet","Olympia","Olympus","Panasonic ","Philips","Polaroid ","RCA","Samsung","SanDisk","Solo","Sony","Toshiba","Vizio ","iRulu"],"Home":["Bodum","Breville","Calphalon","CorningWare","Cuisinart","Delonghi","Dyson","Eureka","Hamilton Beach","KitchenAid","Krups","PUR","Proctor Silex","Pyrex","Simplehuman","Sodastream","T-fal","Waring Pro","Weber","West Bend","oxo"]},"materials":{"Jewelry":["Multi-Tone Gold","Palladium","Platinum","Platinum Plated","Rhodium","Rhodium Plated","Rose Gold","Rose Gold Filled","Rose Gold Plated","Silver Plated","Sterling Silver","Titanium","Titanium Plated","Tungsten","White Gold","White Gold Filled","White Gold Plated","Yellow Gold","Yellow Gold Filled","Yellow Gold Plated","Stainless Steel","Alloy Metal","Other"]},"gemstones":{"Gemstones":["Agate","Alexandrite","Amazonite","Amber","Amethyst","Ametrine","Ammolite","Andalusite","Apatite","Aquamarine","Aventurine","Azotic Topaz","Beryl","Bloodstone","Calcite","Carnelian","Cat's Eye","Chalcedony","Charoite","Chrysocolla","Chrysoprase","Citrine","Coral","Cubic Zirconia","Diamond","Emerald","Fire Opal","Fluorite","Fossil Coral","Garnet","Gaspeite","Goldstone","Goshenite","Hematite","Howlite","Imperial Topaz","Iolite","Jadeite","Jasper","Kunzite","Kyanite","Labradorite","Labradorite","Lapis Lazuli","Larimar","Lepidolite","Malachite","Moonstone","Morganite","Mystic Quartz","Mystic Topaz","Obsidian","Onyx","Opal","Opal","Opal Doublet","Pearl","Peridot","Pietersite","Prehnite","Quartz","Rainbow Moonstone","Rainbow Pyrite","Rhodochrosite","Rose Quartz","Rubellite Tourmaline","Ruby","Ruby-in-Fuchsite","Ruby-Zoisite","Rutile Quartz","Rutile Topaz","Sapphire","Seraphinite","Serpentine","Smoky Quartz","Snowflake Obsidian","Sodalite","Spinel","Star Diopside","Star Garnet","Star Lemon Quartz","Star Moonstone","Star Rose Quartz","Star Ruby","Star Sapphire","Star Sunstone","Sugilite","Sunstone","Tanzanite","Tiger's Eye","Topaz","Tourmaline","Turquoise","Variscite","Verdite","Zircon"]}}
@@ -0,0 +1,12 @@
1
+ HTTP/1.1 200 OK
2
+ X-Frame-Options: SAMEORIGIN
3
+ X-XSS-Protection: 1; mode=block
4
+ X-Content-Type-Options: nosniff
5
+ Content-Type: application/json; charset=utf-8
6
+ ETag: W/"9d4c524f00f493b9214bb3650be52a2f"
7
+ Cache-Control: max-age=0, private, must-revalidate
8
+ X-Request-Id: 08e19f7c-e9cd-427c-9b3f-0f9c04aaae07
9
+ X-Runtime: 0.097894
10
+ Transfer-Encoding: chunked
11
+
12
+ ["New","New with Tags","New with Defects","Used","Used - Excellent","Used - Good","Used - Fair","Refurbished","Refurbished - Manufacturer","Refurbished - Seller"]
@@ -0,0 +1,12 @@
1
+ HTTP/1.1 200 OK
2
+ X-Frame-Options: SAMEORIGIN
3
+ X-XSS-Protection: 1; mode=block
4
+ X-Content-Type-Options: nosniff
5
+ Content-Type: application/json; charset=utf-8
6
+ ETag: W/"9d4c524f00f493b9214bb3650be52a2f"
7
+ Cache-Control: max-age=0, private, must-revalidate
8
+ X-Request-Id: 08e19f7c-e9cd-427c-9b3f-0f9c04aaae07
9
+ X-Runtime: 0.097894
10
+ Transfer-Encoding: chunked
11
+
12
+ {"Gemstones":["Agate","Alexandrite","Amazonite","Amber","Amethyst","Ametrine","Ammolite","Andalusite","Apatite","Aquamarine","Aventurine","Azotic Topaz","Beryl","Bloodstone","Calcite","Carnelian","Cat's Eye","Chalcedony","Charoite","Chrysocolla","Chrysoprase","Citrine","Coral","Cubic Zirconia","Diamond","Emerald","Fire Opal","Fluorite","Fossil Coral","Garnet","Gaspeite","Goldstone","Goshenite","Hematite","Howlite","Imperial Topaz","Iolite","Jadeite","Jasper","Kunzite","Kyanite","Labradorite","Labradorite","Lapis Lazuli","Larimar","Lepidolite","Malachite","Moonstone","Morganite","Mystic Quartz","Mystic Topaz","Obsidian","Onyx","Opal","Opal","Opal Doublet","Pearl","Peridot","Pietersite","Prehnite","Quartz","Rainbow Moonstone","Rainbow Pyrite","Rhodochrosite","Rose Quartz","Rubellite Tourmaline","Ruby","Ruby-in-Fuchsite","Ruby-Zoisite","Rutile Quartz","Rutile Topaz","Sapphire","Seraphinite","Serpentine","Smoky Quartz","Snowflake Obsidian","Sodalite","Spinel","Star Diopside","Star Garnet","Star Lemon Quartz","Star Moonstone","Star Rose Quartz","Star Ruby","Star Sapphire","Star Sunstone","Sugilite","Sunstone","Tanzanite","Tiger's Eye","Topaz","Tourmaline","Turquoise","Variscite","Verdite","Zircon"]}
@@ -0,0 +1,27 @@
1
+ # -*- encoding: utf-8 -*-
2
+ $LOAD_PATH.push File.expand_path('../lib', __FILE__)
3
+ require 'tophatter_merchant/version'
4
+
5
+ Gem::Specification.new do |s|
6
+ s.name = 'tophatter-merchant'
7
+ s.version = TophatterMerchant::VERSION
8
+ s.platform = Gem::Platform::RUBY
9
+ s.licenses = ['MIT']
10
+ s.authors = ['Chris Estreich']
11
+ s.email = ['chris@tophatter.com']
12
+ s.homepage = 'https://github.com/tophatter/merchant-api-ruby'
13
+ s.summary = 'Manage your inventory and fulfill orders on Tophatter.'
14
+ s.description = 'The Tophatter merchant platform is an e-commerce platform. It allows merchants to manage inventory and fulfill orders on Tophatter.'
15
+
16
+ s.required_ruby_version = '~> 2.0'
17
+
18
+ s.add_dependency 'rest-client', '~> 1.6'
19
+ s.add_dependency 'activemodel', '~> 4.2'
20
+
21
+ s.post_install_message = 'Documentation is available at: https://tophatter.readme.io/'
22
+
23
+ s.files = `git ls-files`.split("\n")
24
+ s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
25
+ s.executables = `git ls-files -- bin/*`.split("\n").map { |f| File.basename(f) }
26
+ s.require_paths = ['lib']
27
+ end
metadata ADDED
@@ -0,0 +1,103 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: tophatter-merchant
3
+ version: !ruby/object:Gem::Version
4
+ version: '1.0'
5
+ platform: ruby
6
+ authors:
7
+ - Chris Estreich
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2016-11-23 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: rest-client
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '1.6'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '1.6'
27
+ - !ruby/object:Gem::Dependency
28
+ name: activemodel
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '4.2'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '4.2'
41
+ description: The Tophatter merchant platform is an e-commerce platform. It allows
42
+ merchants to manage inventory and fulfill orders on Tophatter.
43
+ email:
44
+ - chris@tophatter.com
45
+ executables:
46
+ - console
47
+ extensions: []
48
+ extra_rdoc_files: []
49
+ files:
50
+ - ".gitignore"
51
+ - ".rspec"
52
+ - CHANGELOG
53
+ - Gemfile
54
+ - LICENSE
55
+ - README.md
56
+ - VERSION
57
+ - bin/console
58
+ - lib/tophatter_merchant.rb
59
+ - lib/tophatter_merchant/account.rb
60
+ - lib/tophatter_merchant/api_key.rb
61
+ - lib/tophatter_merchant/exceptions.rb
62
+ - lib/tophatter_merchant/image.rb
63
+ - lib/tophatter_merchant/metadata.rb
64
+ - lib/tophatter_merchant/order.rb
65
+ - lib/tophatter_merchant/product.rb
66
+ - lib/tophatter_merchant/resource.rb
67
+ - lib/tophatter_merchant/variation.rb
68
+ - lib/tophatter_merchant/version.rb
69
+ - spec/spec_helper.rb
70
+ - spec/tophatter_merchant/metadata_spec.rb
71
+ - spec/tophatter_merchant/product_spec.rb
72
+ - tmp/stubs/metadata.json
73
+ - tmp/stubs/metadata/conditions.json
74
+ - tmp/stubs/metadata/gemstones.json
75
+ - tophatter-merchant.gemspec
76
+ homepage: https://github.com/tophatter/merchant-api-ruby
77
+ licenses:
78
+ - MIT
79
+ metadata: {}
80
+ post_install_message: 'Documentation is available at: https://tophatter.readme.io/'
81
+ rdoc_options: []
82
+ require_paths:
83
+ - lib
84
+ required_ruby_version: !ruby/object:Gem::Requirement
85
+ requirements:
86
+ - - "~>"
87
+ - !ruby/object:Gem::Version
88
+ version: '2.0'
89
+ required_rubygems_version: !ruby/object:Gem::Requirement
90
+ requirements:
91
+ - - ">="
92
+ - !ruby/object:Gem::Version
93
+ version: '0'
94
+ requirements: []
95
+ rubyforge_project:
96
+ rubygems_version: 2.4.8
97
+ signing_key:
98
+ specification_version: 4
99
+ summary: Manage your inventory and fulfill orders on Tophatter.
100
+ test_files:
101
+ - spec/spec_helper.rb
102
+ - spec/tophatter_merchant/metadata_spec.rb
103
+ - spec/tophatter_merchant/product_spec.rb