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.
@@ -0,0 +1 @@
1
+ require 'bliss/client'
@@ -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,12 @@
1
+ module Bliss
2
+ module Client
3
+ class Color
4
+ include Virtus.model
5
+
6
+ attribute :id, Integer
7
+ attribute :name, String
8
+ attribute :rank, Integer
9
+ attribute :program_id, Integer
10
+ end
11
+ end
12
+ 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,12 @@
1
+ module Bliss
2
+ module Client
3
+ class LookItem
4
+ include Virtus.model
5
+
6
+ attribute :style_description, String
7
+ attribute :color_name, String
8
+ attribute :style_id, Integer
9
+ attribute :color_id, Integer
10
+ end
11
+ end
12
+ 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,14 @@
1
+ module Bliss
2
+ module Client
3
+ class Program
4
+ include Virtus.model
5
+
6
+ attribute :id, Integer
7
+ attribute :name, String
8
+ attribute :collection_id, Integer
9
+
10
+ attribute :colors, Array[Color]
11
+ attribute :styles, Array[Style]
12
+ end
13
+ end
14
+ 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