bliss-client 1.2.0
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 +7 -0
- data/.env.example +3 -0
- data/.gitignore +10 -0
- data/.ruby-version +1 -0
- data/.travis.yml +4 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +21 -0
- data/README.md +205 -0
- data/Rakefile +10 -0
- data/bin/console +13 -0
- data/bin/setup +7 -0
- data/bliss-client.gemspec +43 -0
- data/fixtures/vcr_cassettes/test_connection_speaks_with_api.yml +48 -0
- data/fixtures/vcr_cassettes/test_create_creates_an_order_in_bliss_and_sets_id.yml +91 -0
- data/fixtures/vcr_cassettes/test_fetches_all_sizes.yml +93 -0
- data/fixtures/vcr_cassettes/test_find_collections_returns_array_of_collections.yml +126 -0
- data/fixtures/vcr_cassettes/test_include_articles.yml +2092 -0
- data/fixtures/vcr_cassettes/test_include_colors.yml +162 -0
- data/fixtures/vcr_cassettes/test_include_programs.yml +113 -0
- data/fixtures/vcr_cassettes/test_include_styles.yml +390 -0
- data/lib/bliss-client.rb +1 -0
- data/lib/bliss/client.rb +97 -0
- data/lib/bliss/client/address.rb +21 -0
- data/lib/bliss/client/article.rb +17 -0
- data/lib/bliss/client/collection.rb +86 -0
- data/lib/bliss/client/color.rb +12 -0
- data/lib/bliss/client/item.rb +16 -0
- data/lib/bliss/client/look.rb +63 -0
- data/lib/bliss/client/look_item.rb +12 -0
- data/lib/bliss/client/order.rb +52 -0
- data/lib/bliss/client/program.rb +14 -0
- data/lib/bliss/client/size.rb +33 -0
- data/lib/bliss/client/style.rb +77 -0
- data/lib/bliss/client/validation.rb +15 -0
- data/lib/bliss/client/version.rb +5 -0
- metadata +267 -0
data/lib/bliss-client.rb
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
require 'bliss/client'
|
data/lib/bliss/client.rb
ADDED
@@ -0,0 +1,97 @@
|
|
1
|
+
require 'faraday'
|
2
|
+
require 'faraday_middleware'
|
3
|
+
require 'typhoeus/adapters/faraday'
|
4
|
+
require 'virtus'
|
5
|
+
require 'active_model'
|
6
|
+
|
7
|
+
require_relative 'client/version'
|
8
|
+
require_relative 'client/validation'
|
9
|
+
require_relative 'client/item'
|
10
|
+
require_relative 'client/address'
|
11
|
+
require_relative 'client/order'
|
12
|
+
require_relative 'client/size'
|
13
|
+
require_relative 'client/article'
|
14
|
+
require_relative 'client/style'
|
15
|
+
require_relative 'client/color'
|
16
|
+
require_relative 'client/program'
|
17
|
+
require_relative 'client/look_item'
|
18
|
+
require_relative 'client/look'
|
19
|
+
require_relative 'client/collection'
|
20
|
+
|
21
|
+
module Bliss
|
22
|
+
module Client
|
23
|
+
module_function
|
24
|
+
|
25
|
+
ADAPTER = :typhoeus
|
26
|
+
API_VERSION = ENV['BLISS_API_VERSION'] || 'v1'
|
27
|
+
TOKEN_PATH = '/oauth/token'
|
28
|
+
|
29
|
+
def connection
|
30
|
+
@connection = nil if test?
|
31
|
+
|
32
|
+
@connection ||= Faraday.new(url: api_url) do |f|
|
33
|
+
f.request :oauth2, token unless skip_auth?
|
34
|
+
f.request :json
|
35
|
+
f.response :json, content_type: /\bjson$/
|
36
|
+
if debug?
|
37
|
+
f.response :logger
|
38
|
+
end
|
39
|
+
f.adapter ADAPTER
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
def token
|
44
|
+
# Fetch the token every time when testing, so that VCR can expect this
|
45
|
+
# request to happen.
|
46
|
+
@token = nil if test?
|
47
|
+
|
48
|
+
@token ||= begin
|
49
|
+
connection = Faraday.new(url: server) do |f|
|
50
|
+
f.request :json
|
51
|
+
f.response :json, content_type: /\bjson$/
|
52
|
+
if debug?
|
53
|
+
f.response :logger
|
54
|
+
end
|
55
|
+
f.adapter ADAPTER
|
56
|
+
end
|
57
|
+
|
58
|
+
response = connection.post(
|
59
|
+
TOKEN_PATH,
|
60
|
+
grant_type: 'client_credentials',
|
61
|
+
client_id: client_id,
|
62
|
+
client_secret: client_secret
|
63
|
+
)
|
64
|
+
|
65
|
+
response.body.fetch('access_token')
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
def api_url
|
70
|
+
"#{server}/api/#{API_VERSION}"
|
71
|
+
end
|
72
|
+
|
73
|
+
def server
|
74
|
+
ENV['BLISS_SERVER']
|
75
|
+
end
|
76
|
+
|
77
|
+
def client_id
|
78
|
+
ENV['BLISS_CLIENT_ID']
|
79
|
+
end
|
80
|
+
|
81
|
+
def client_secret
|
82
|
+
ENV['BLISS_CLIENT_SECRET']
|
83
|
+
end
|
84
|
+
|
85
|
+
def debug?
|
86
|
+
ENV['BLISS_CLIENT_DEBUG']
|
87
|
+
end
|
88
|
+
|
89
|
+
def test?
|
90
|
+
ENV['BLISS_CLIENT_ENV'] == 'test'
|
91
|
+
end
|
92
|
+
|
93
|
+
def skip_auth?
|
94
|
+
ENV['BLISS_CLIENT_SKIP_AUTH'] == 'true'
|
95
|
+
end
|
96
|
+
end
|
97
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
module Bliss
|
2
|
+
module Client
|
3
|
+
class Address
|
4
|
+
include Virtus.value_object
|
5
|
+
include Validation
|
6
|
+
|
7
|
+
values do
|
8
|
+
attribute :salutation, String
|
9
|
+
attribute :line_1, String
|
10
|
+
attribute :line_2, String
|
11
|
+
attribute :line_3, String
|
12
|
+
attribute :zip_code, String
|
13
|
+
attribute :city, String
|
14
|
+
attribute :country_iso3, String
|
15
|
+
end
|
16
|
+
|
17
|
+
validates_presence_of :salutation, :line_1, :zip_code, :city,
|
18
|
+
:country_iso3
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
module Bliss
|
2
|
+
module Client
|
3
|
+
class Article
|
4
|
+
include Virtus.model
|
5
|
+
|
6
|
+
attribute :id, Integer
|
7
|
+
attribute :ean, String
|
8
|
+
attribute :color_name, String
|
9
|
+
attribute :size_name, String
|
10
|
+
attribute :style_name, String
|
11
|
+
attribute :legacy_id, Integer
|
12
|
+
attribute :color_id, Integer
|
13
|
+
attribute :size_id, Integer
|
14
|
+
attribute :style_id, Integer
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,86 @@
|
|
1
|
+
module Bliss
|
2
|
+
module Client
|
3
|
+
class Collection
|
4
|
+
include Virtus.model
|
5
|
+
|
6
|
+
attribute :id, Integer
|
7
|
+
attribute :name, String
|
8
|
+
attribute :year, Integer
|
9
|
+
attribute :season, Integer
|
10
|
+
attribute :men, Boolean
|
11
|
+
|
12
|
+
attribute :programs, Array[Program]
|
13
|
+
|
14
|
+
def self.all
|
15
|
+
response = Client.connection.get('collections')
|
16
|
+
if response.success?
|
17
|
+
response.body.map { |attr| new attr }
|
18
|
+
else
|
19
|
+
raise JSON.parse(response.body).fetch('message')
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
# Fetch collections and their children.
|
24
|
+
#
|
25
|
+
# @param [Array<Integer>] ids The ids of the collections to fetch
|
26
|
+
# @param [Hash] options
|
27
|
+
# @option options [Boolean] (false) :include_programs Whether to fetch
|
28
|
+
# programs for each collection
|
29
|
+
# @option options [Boolean] (false) :include_colors Whether to fetch
|
30
|
+
# colors for each program
|
31
|
+
# @option options [Boolean] (false) :include_styles Whether to fetch
|
32
|
+
# styles for each program
|
33
|
+
# @option options [Boolean] (false) :include_articles Whether to fetch
|
34
|
+
# articles for each style
|
35
|
+
# @option options [Boolean] (false) :include_prices Whether to fetch
|
36
|
+
# prices for each style
|
37
|
+
#
|
38
|
+
# @return [Array<Collection>]
|
39
|
+
#
|
40
|
+
# @example Fetch collections 88 and 90 with programs, styles and prices
|
41
|
+
# collections = Bliss::Client::Collection.find(
|
42
|
+
# [88, 90],
|
43
|
+
# include_programs: true,
|
44
|
+
# include_styles: true,
|
45
|
+
# include_prices: true
|
46
|
+
# )
|
47
|
+
#
|
48
|
+
def self.find(ids, options = {})
|
49
|
+
options = {
|
50
|
+
include_programs: false,
|
51
|
+
include_styles: false,
|
52
|
+
include_articles: false,
|
53
|
+
include_colors: false,
|
54
|
+
include_prices: false
|
55
|
+
}.merge(options)
|
56
|
+
|
57
|
+
c = Client.connection
|
58
|
+
responses_hash = {}
|
59
|
+
|
60
|
+
c.in_parallel do
|
61
|
+
ids.each do |id|
|
62
|
+
responses_hash[id] = c.get("collections/#{id}", options)
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
responses = responses_hash.values
|
67
|
+
|
68
|
+
if (failed_response = responses.find { |r| !r.success? })
|
69
|
+
raise JSON.parse(failed_response.body).fetch('message')
|
70
|
+
end
|
71
|
+
|
72
|
+
responses.map { |r| new r.body }
|
73
|
+
end
|
74
|
+
|
75
|
+
# @return [Array<Look>] All keylooks for this collection
|
76
|
+
def keylooks
|
77
|
+
@keylooks ||= Look.for_collection(id, type: :keylook)
|
78
|
+
end
|
79
|
+
|
80
|
+
# @return [Array<Look>] All studiolooks for this collection
|
81
|
+
def studiolooks
|
82
|
+
@studiolooks ||= Look.for_collection(id, type: :studiolook)
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
module Bliss
|
2
|
+
module Client
|
3
|
+
class Item
|
4
|
+
include Virtus.model
|
5
|
+
include Validation
|
6
|
+
|
7
|
+
attribute :article_id, Integer
|
8
|
+
attribute :price_value, BigDecimal
|
9
|
+
attribute :price_currency, String
|
10
|
+
attribute :quantity, Integer
|
11
|
+
|
12
|
+
validates_presence_of :article_id, :price_value, :price_currency,
|
13
|
+
:quantity
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
@@ -0,0 +1,63 @@
|
|
1
|
+
module Bliss
|
2
|
+
module Client
|
3
|
+
class Look
|
4
|
+
include Virtus.model
|
5
|
+
|
6
|
+
attribute :id, Integer
|
7
|
+
attribute :number, String
|
8
|
+
attribute :photo, String
|
9
|
+
attribute :collection_id, Integer
|
10
|
+
attribute :descriptions, Hash
|
11
|
+
|
12
|
+
attribute :items, Array[LookItem]
|
13
|
+
|
14
|
+
RESOURCES = {
|
15
|
+
keylook: 'keylooks',
|
16
|
+
studiolook: 'studiolooks'
|
17
|
+
}
|
18
|
+
|
19
|
+
# Fetch looks for a collection.
|
20
|
+
#
|
21
|
+
# @param [Integer] collection_id The id of the collection
|
22
|
+
# @param [Hash] args
|
23
|
+
# @option args [Symbol] :type Which kind of look to fetch; either
|
24
|
+
# :keylook or :studiolook
|
25
|
+
#
|
26
|
+
# @return [Array<Look>]
|
27
|
+
#
|
28
|
+
# @example Fetch all keylooks for the collection with id 88
|
29
|
+
# Bliss::Client::Look.for_collection(88, type: :keylook)
|
30
|
+
#
|
31
|
+
def self.for_collection(collection_id, args)
|
32
|
+
resource = RESOURCES.fetch(args.fetch(:type).to_sym)
|
33
|
+
url = "collections/#{collection_id}/#{resource}"
|
34
|
+
|
35
|
+
response = Client.connection.get url
|
36
|
+
|
37
|
+
if response.success?
|
38
|
+
response.body.map { |attr| new attr }
|
39
|
+
else
|
40
|
+
raise JSON.parse(response.body).fetch('message')
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
# Get description in language.
|
45
|
+
#
|
46
|
+
# @param [Symbol] language ISO 639-1 two-letter languange code
|
47
|
+
# @return [String] description of this look in the requested language
|
48
|
+
#
|
49
|
+
# @example Get English description
|
50
|
+
# look.description(:en) # => "Super trendy-wendy look combining yada..."
|
51
|
+
def description(language)
|
52
|
+
descriptions.fetch(language.to_s.downcase)
|
53
|
+
rescue KeyError
|
54
|
+
raise ArgumentError, %{
|
55
|
+
No description found in language: "#{language}".
|
56
|
+
|
57
|
+
Make sure "#{language}" is a valid ISO 639-1 two-letter code.
|
58
|
+
See https://en.wikipedia.org/wiki/List_of_ISO_639-1_codes
|
59
|
+
}
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
@@ -0,0 +1,52 @@
|
|
1
|
+
module Bliss
|
2
|
+
module Client
|
3
|
+
class Order
|
4
|
+
include Virtus.model
|
5
|
+
include Validation
|
6
|
+
|
7
|
+
attribute :id, Integer
|
8
|
+
attribute :type, String
|
9
|
+
attribute :items, Array[Item]
|
10
|
+
attribute :delivery_address, Address
|
11
|
+
attribute :remote_id, Integer
|
12
|
+
attribute :blizzard_customer_id, Integer
|
13
|
+
attribute :created_at, DateTime
|
14
|
+
|
15
|
+
validates_presence_of :type, :items, :delivery_address,
|
16
|
+
:remote_id, :blizzard_customer_id
|
17
|
+
|
18
|
+
def self.create(attributes)
|
19
|
+
order = new(attributes)
|
20
|
+
order.create
|
21
|
+
order
|
22
|
+
end
|
23
|
+
|
24
|
+
def create
|
25
|
+
validate!
|
26
|
+
|
27
|
+
response = Client.connection.post(
|
28
|
+
'orders',
|
29
|
+
blizzard_customer_id: blizzard_customer_id,
|
30
|
+
type: type,
|
31
|
+
items: items.map(&:attributes),
|
32
|
+
delivery_address: delivery_address.attributes,
|
33
|
+
remote_id: remote_id
|
34
|
+
)
|
35
|
+
|
36
|
+
if response.success?
|
37
|
+
body = response.body
|
38
|
+
self.id = body.fetch('id')
|
39
|
+
self.created_at = body.fetch('created_at')
|
40
|
+
else
|
41
|
+
raise JSON.parse(response.body).fetch('message')
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
def validate!
|
46
|
+
super
|
47
|
+
items.each &:validate!
|
48
|
+
delivery_address.validate!
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
module Bliss
|
2
|
+
module Client
|
3
|
+
class Size
|
4
|
+
include Virtus.value_object
|
5
|
+
|
6
|
+
values do
|
7
|
+
attribute :id
|
8
|
+
attribute :name
|
9
|
+
attribute :rank
|
10
|
+
end
|
11
|
+
|
12
|
+
def self.all
|
13
|
+
@all ||= begin
|
14
|
+
response = Client.connection.get('sizes')
|
15
|
+
if response.success?
|
16
|
+
body = response.body
|
17
|
+
body.map { |attr| new attr }
|
18
|
+
else
|
19
|
+
raise JSON.parse(response.body).fetch('message')
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
def self.find(id)
|
25
|
+
all.find { |size| size.id == id }
|
26
|
+
end
|
27
|
+
|
28
|
+
def to_s
|
29
|
+
"#{rank} | #{name}"
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
@@ -0,0 +1,77 @@
|
|
1
|
+
require 'bigdecimal'
|
2
|
+
|
3
|
+
module Bliss
|
4
|
+
module Client
|
5
|
+
class Style
|
6
|
+
include Virtus.model
|
7
|
+
|
8
|
+
attribute :id, Integer
|
9
|
+
attribute :number, String
|
10
|
+
attribute :name, String
|
11
|
+
attribute :treatment, String
|
12
|
+
attribute :form, String
|
13
|
+
attribute :quality, String
|
14
|
+
attribute :category, String
|
15
|
+
attribute :collection_id, Integer
|
16
|
+
attribute :program_id, Integer
|
17
|
+
attribute :prices, Hash
|
18
|
+
|
19
|
+
attribute :articles, Array[Article]
|
20
|
+
|
21
|
+
# @return [BigDecimal] the current purchase (aka wholesale) price, i.e.
|
22
|
+
# the price the stores pay to buy this style from NILE
|
23
|
+
#
|
24
|
+
# @param [Symbol] currency either :eur or :chf, can also be strings
|
25
|
+
# e.g. 'CHF'
|
26
|
+
def purchase_price(currency)
|
27
|
+
BigDecimal.new prices_for_currency(currency).
|
28
|
+
fetch('current_purchase_price')
|
29
|
+
end
|
30
|
+
|
31
|
+
# @return [BigDecimal] the current sales (aka retail) price, i.e. the
|
32
|
+
# price the customers pay to buy this style from a store/webshop
|
33
|
+
#
|
34
|
+
# @param [Symbol] currency either :eur or :chf, can also be strings
|
35
|
+
# e.g. 'CHF'
|
36
|
+
def sales_price(currency)
|
37
|
+
BigDecimal.new prices_for_currency(currency).
|
38
|
+
fetch('current_sales_price')
|
39
|
+
end
|
40
|
+
|
41
|
+
# @return [BigDecimal] the first unreduced price stores paid to purchase
|
42
|
+
# this style from NILE
|
43
|
+
#
|
44
|
+
# @param [Symbol] currency either :eur or :chf, can also be strings
|
45
|
+
# e.g. 'CHF'
|
46
|
+
def original_purchase_price(currency)
|
47
|
+
BigDecimal.new prices_for_currency(currency).
|
48
|
+
fetch('first_purchase_price')
|
49
|
+
end
|
50
|
+
|
51
|
+
# @return [BigDecimal] the first unreduced price customers paid to purchase
|
52
|
+
# this style from a store/webshop
|
53
|
+
#
|
54
|
+
# @param [Symbol] currency either :eur or :chf, can also be strings
|
55
|
+
# e.g. 'CHF'
|
56
|
+
def original_sales_price(currency)
|
57
|
+
BigDecimal.new prices_for_currency(currency).
|
58
|
+
fetch('first_sales_price')
|
59
|
+
end
|
60
|
+
|
61
|
+
# @return [Boolean] whether this style is sold for a reduced price for
|
62
|
+
# the currency
|
63
|
+
#
|
64
|
+
# @param [Symbol] currency either :eur or :chf, can also be strings
|
65
|
+
# e.g. 'CHF'
|
66
|
+
def discounted?(currency)
|
67
|
+
prices_for_currency(currency).fetch('current_price_rank').to_i > 1
|
68
|
+
end
|
69
|
+
|
70
|
+
private
|
71
|
+
|
72
|
+
def prices_for_currency(currency)
|
73
|
+
prices.fetch(currency.to_s.upcase)
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|