erp_integration 0.52.0 → 0.53.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 7984c5549446febb8478fe9611b601c257a0bd03b3642ec5d35b480f504cbff5
4
- data.tar.gz: 540f6ca9033aebe017cb2c32db0d21de595ea172a42316e5e3366fe933a8739f
3
+ metadata.gz: 7719f3eaa455812be74cbdb4448118217e8294c90e742bb9d4074fad423a10ab
4
+ data.tar.gz: 35e221932e9c229957951869daa324c7f40549bf1cdfe21d7c56dc3202741832
5
5
  SHA512:
6
- metadata.gz: 36d578d1ac7e5bfaacd79118fc1f5405e5a754e4a6a3571bc0957aa7347b159beafdce7bcaf27f968df2a18b7a736f63956426b1db3ca5c99ec923102a2db592
7
- data.tar.gz: d985ee23e864310a16034cb25c360777be1b172ca4160c707a63ea387e4623e323c88c16ff8330763bee7cc122b78bc4c516fd0e36736924d88eae4a33b312ec
6
+ metadata.gz: e9835ef2f7400c112999ac8ca0135d7f11be0c69129bd4403f5620042b06394e09a6ec62c225d9d82d41d66b7d6290a602edf525d48fe910e92ab48562456711
7
+ data.tar.gz: 4a39d845dda9398b971334392fb58539a3cc208896a19e608c2207ddc040331b9e2a27ffef6ff1bcdc1ab56df4ba4a5de1a6da85399d37ca53a3b795f762771f
@@ -15,4 +15,4 @@ jobs:
15
15
  steps:
16
16
  - uses: seferov/pr-lint-action@master
17
17
  with:
18
- title-regex: '^\[RELEASE|NO-TICKET|DL|UDL|AD|DU|AR|IN|CON|DIS|AS|MT|SCR|RO|DEL|BE-\d*\](\ )'
18
+ title-regex: '^\[RELEASE|NO-TICKET|DL|UDL|AD|DU|AR|IN|CON|DIS|AS|MT|SCR|RO|DEL|BE|STORE-\d*\](\ )'
data/README.md CHANGED
@@ -25,11 +25,42 @@ To configure the gem, create an initializer and add the following lines:
25
25
  ```erb
26
26
  # config/initializers/erp_integration.rb
27
27
  ErpIntegration.configure do |config|
28
- config.fulfil_api_key = '<your-api-key>'
28
+ config.fulfil_api_keys = '<your-api-key>'
29
29
  config.fulfil_merchant_id = '<your-merchant-id>'
30
30
  end
31
31
  ```
32
32
 
33
+ You can configure multiple API keys, to enable rotation mechanism (see ["API key rotation"](README.md:L42)).
34
+ ```erb
35
+ # config/initializers/erp_integration.rb
36
+ ErpIntegration.configure do |config|
37
+ config.fulfil_api_keys = ['<your-api-key1>', '<your-api-key2>']
38
+ config.fulfil_merchant_id = '<your-merchant-id>'
39
+ end
40
+ ```
41
+
42
+ ### API keys rotation
43
+
44
+ To set up API key rotation, configure multiple keys.
45
+ Every time a Fulfil client receives a `404` or `429`, it will rotate an API key.
46
+
47
+ ```erb
48
+ # config/initializers/erp_integration.rb
49
+ ErpIntegration.configure do |config|
50
+ config.fulfil_api_keys = ['<your-api-key1>', '<your-api-key2>']
51
+ ...
52
+ end
53
+ ```
54
+
55
+ You can also configure a separate group of API keys for each resource.
56
+ From this point, `ErpIntegration::SalesOrder` will use custom API keys, and the other resources will continue to use the configured API keys pool.
57
+
58
+ ```ruby
59
+ ErpIntegration::SalesOrder.api_keys_pool = '<your-api-key>'
60
+ # or
61
+ ErpIntegration::SalesOrder.api_keys_pool = ['<your-api-key1>', '<your-api-key2>']
62
+ ```
63
+
33
64
  ### Supported Query Methods
34
65
 
35
66
  After configuring the gem, one can easily query all the available ERP resources from the connected third-parties. In all cases, the API will return a collection of resources.
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ErpIntegration
4
+ # The `ApiKeysPool` class is a simple
5
+ # class that holds a list of API keys
6
+ # and rotates between them.
7
+ class ApiKeysPool
8
+ attr_reader :api_keys
9
+
10
+ def initialize
11
+ @api_keys = []
12
+ @current_key_index = 0
13
+ end
14
+
15
+ # Allows setting a list of API keys for the pool.
16
+ def api_keys=(keys)
17
+ @api_keys = Array(keys)
18
+ end
19
+
20
+ # Returns the current API key.
21
+ # @return [String] The current API key.
22
+ def current_key
23
+ @api_keys[@current_key_index]
24
+ end
25
+
26
+ # Rotates the API key to the next one in the list.
27
+ def rotate_key!
28
+ @current_key_index = (@current_key_index + 1) % @api_keys.size
29
+ end
30
+ end
31
+ end
@@ -8,14 +8,14 @@ module ErpIntegration
8
8
  # ```ruby
9
9
  # # config/initializers/erp_integration.rb
10
10
  # ErpIntegration.configure do |config|
11
- # config.fulfil_api_key = "..."
11
+ # config.fulfil_api_keys = "..."
12
12
  # end
13
13
  # ```
14
14
  class Configuration
15
- # The `fulfil_api_key` is used by the `FulfilClient` to authorize the
16
- # requests to the Fulfil API endpoints.
17
- # @return [String] The API key for Fulfil.
18
- attr_accessor :fulfil_api_key
15
+ # The `fulfil_api_keys` sets a single or a list of API keys to use
16
+ # in the `FulfilClient` to authorize the requests to the Fulfil API endpoints.
17
+ # @return [Array<Atring>] The API keys for Fulfil.
18
+ attr_accessor :fulfil_api_keys
19
19
 
20
20
  # The `fulfil_merchant_id` is used by the `FulfilClient` to connect to
21
21
  # the right Fulfil API endpoints.
@@ -17,7 +17,7 @@ module ErpIntegration
17
17
  include QueryMethods
18
18
 
19
19
  attr_accessor :resource_klass
20
- delegate :client, :model_name, to: 'self.class'
20
+ delegate :api_keys_pool=, :client, :model_name, to: 'self.class'
21
21
 
22
22
  def initialize(resource_klass)
23
23
  @resource_klass = resource_klass
@@ -27,12 +27,24 @@ module ErpIntegration
27
27
  # @return [ErpIntegration::Fulfil::Client] The HTTP client for Fulfil.
28
28
  def self.client
29
29
  Client.new(
30
- api_key: config.fulfil_api_key,
30
+ api_keys_pool: api_keys_pool,
31
31
  merchant_id: config.fulfil_merchant_id,
32
32
  logger: config.logger
33
33
  )
34
34
  end
35
35
 
36
+ # The `api_keys_pool` exposes the API keys pool to the class.
37
+ # @return [ErpIntegration::ApiKeysPool] The API keys pool object.
38
+ def self.api_keys_pool
39
+ @api_keys_pool ||= ApiKeysPool.new.tap { |pool| pool.api_keys = config.fulfil_api_keys }
40
+ end
41
+
42
+ # Allows setting a new API keys pool for the `ApiResource`
43
+ # @return [ErpIntegration::ApiKeysPool] ApiKeysPool object with the new API keys.
44
+ def self.api_keys_pool=(fulfil_api_keys)
45
+ @api_keys_pool = ApiKeysPool.new.tap { |pool| pool.api_keys = fulfil_api_keys }
46
+ end
47
+
36
48
  # The `config` exposes the gem's configuration to the `ApiResource`.
37
49
  # @return [ErpIntegration::Configuration] The gem's configuration object.
38
50
  def self.config
@@ -60,19 +72,16 @@ module ErpIntegration
60
72
  # be executed and the results will be fetched.
61
73
  # @return [Array] An enumerable collection object with all API results.
62
74
  def all
63
- return @results if defined?(@results)
64
-
65
- @results =
66
- client.put(
67
- api_resource_path,
68
- Query.new(
69
- fields: selected_fields,
70
- filters: where_clauses,
71
- alternative_filters: or_clauses,
72
- limit: limit_value,
73
- offset: calculated_offset
74
- )
75
- ).map { |item| resource_klass.new(item) }
75
+ client.put(
76
+ api_resource_path,
77
+ Query.new(
78
+ fields: selected_fields,
79
+ filters: where_clauses,
80
+ alternative_filters: or_clauses,
81
+ limit: limit_value,
82
+ offset: calculated_offset
83
+ )
84
+ ).map { |item| resource_klass.new(item) }
76
85
  end
77
86
 
78
87
  # As with the `all` method, the `query methods` lazyly build a search/read
@@ -81,17 +90,14 @@ module ErpIntegration
81
90
  # the result will be fetched
82
91
  # @return [Integer] The count of records that match with the query in Fulfil
83
92
  def count
84
- return @count if defined?(@count)
85
-
86
- @count =
87
- client.put(
88
- "model/#{model_name}/search_count",
89
- Query.new(
90
- fields: nil,
91
- filters: where_clauses,
92
- alternative_filters: or_clauses
93
- ).to_h.except(:fields)
94
- )
93
+ client.put(
94
+ "model/#{model_name}/search_count",
95
+ Query.new(
96
+ fields: nil,
97
+ filters: where_clauses,
98
+ alternative_filters: or_clauses
99
+ ).to_h.except(:fields)
100
+ )
95
101
  end
96
102
 
97
103
  # The `each` method turns the `ApiResource` instance into an enumerable object.
@@ -3,11 +3,11 @@
3
3
  module ErpIntegration
4
4
  module Fulfil
5
5
  class Client
6
- attr_reader :api_key, :merchant_id
7
- attr_writer :connection, :faraday_adapter
6
+ attr_reader :api_keys_pool, :merchant_id
7
+ attr_writer :connection, :faraday_adapter, :rotate_statuses
8
8
 
9
- def initialize(api_key:, merchant_id:, logger: nil)
10
- @api_key = api_key
9
+ def initialize(api_keys_pool:, merchant_id:, logger: nil)
10
+ @api_keys_pool = api_keys_pool
11
11
  @merchant_id = merchant_id
12
12
  @logger = logger
13
13
  end
@@ -36,13 +36,12 @@ module ErpIntegration
36
36
  faraday.response :follow_redirects
37
37
  faraday.response :json # Decode response bodies as JSON
38
38
 
39
- # Notice that logging headers will expose sensitive information
40
- # like api-key. To avoid use filters:
41
- # https://lostisland.github.io/faraday/middleware/logger
42
- faraday.response :logger, @logger, { headers: false, bodies: true } if @logger
43
-
44
39
  # Custom error handling for the error response
45
40
  faraday.use ErpIntegration::Middleware::ErrorHandling
41
+ # Custom middleware for rotating API keys
42
+ faraday.use ErpIntegration::Middleware::ApiKeysRotation, api_keys_pool: api_keys_pool
43
+ # Custom middleware for logging requests and responses
44
+ faraday.use ErpIntegration::Middleware::Logger, @logger if @logger
46
45
 
47
46
  # Adapter definition should be last in order to make the json parsers be loaded correctly
48
47
  faraday.adapter faraday_adapter
@@ -67,11 +66,14 @@ module ErpIntegration
67
66
 
68
67
  private
69
68
 
69
+ def api_key
70
+ api_keys_pool.current_key
71
+ end
72
+
70
73
  def default_headers
71
74
  {
72
75
  'Accept': 'application/json',
73
- 'Content-Type': 'application/json',
74
- 'X-API-KEY': api_key
76
+ 'Content-Type': 'application/json'
75
77
  }
76
78
  end
77
79
  end
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ErpIntegration
4
+ module Middleware
5
+ class ApiKeysRotation < Faraday::Middleware
6
+ HTTP_ROTATE_CODES = [404, 429].freeze
7
+
8
+ attr_reader :options
9
+
10
+ def initialize(app, options = {})
11
+ super(app)
12
+
13
+ @options = options
14
+ end
15
+
16
+ def call(env)
17
+ on_request(env)
18
+
19
+ @app.call(env).on_complete { |response| on_complete(response) }
20
+ end
21
+
22
+ def on_request(env)
23
+ env[:request_headers]['X-API-KEY'] = options[:api_keys_pool].current_key
24
+ end
25
+
26
+ def on_complete(env)
27
+ return unless HTTP_ROTATE_CODES.include?(env.status)
28
+
29
+ options[:api_keys_pool].rotate_key!
30
+ end
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,51 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'faraday/logging/formatter'
4
+
5
+ module ErpIntegration
6
+ module Middleware
7
+ class Logger < Faraday::Middleware
8
+ # Notice that logging headers will expose sensitive information
9
+ # like api-key.
10
+ DEFAULT_OPTIONS = { headers: false, bodies: true, errors: true }.freeze
11
+
12
+ def initialize(app, logger = nil, options = {})
13
+ super(app)
14
+
15
+ @logger = logger
16
+ formatter_klass = options.delete(:formatter) || Faraday::Logging::Formatter
17
+ @formatter = formatter_klass.new(logger: logger, options: DEFAULT_OPTIONS)
18
+ yield @formatter if block_given?
19
+ end
20
+
21
+ def call(env)
22
+ api_key_fragment = sanitize_api_key(env[:request_headers]['X-API-KEY'])
23
+
24
+ if @logger.respond_to?(:tagged)
25
+ @logger.tagged("API key *#{api_key_fragment}") do
26
+ @formatter.request(env)
27
+
28
+ @app.call(env).on_complete { |response| on_complete(response) }
29
+ end
30
+ else
31
+ @logger.info("Requested ERP with API key *#{api_key_fragment}")
32
+ @formatter.request(env)
33
+
34
+ @app.call(env).on_complete { |response| on_complete(response) }
35
+ end
36
+ end
37
+
38
+ def on_complete(env)
39
+ @formatter.response(env)
40
+ end
41
+
42
+ def on_error(exc)
43
+ @formatter.exception(exc) if @formatter.respond_to?(:exception)
44
+ end
45
+
46
+ def sanitize_api_key(api_key)
47
+ api_key[-4..-1] if api_key
48
+ end
49
+ end
50
+ end
51
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module ErpIntegration
4
- VERSION = '0.52.0'
4
+ VERSION = '0.53.0'
5
5
  end
@@ -10,10 +10,13 @@ require 'json'
10
10
 
11
11
  require_relative 'erp_integration/version'
12
12
  require_relative 'erp_integration/errors'
13
+ require_relative 'erp_integration/api_keys_pool'
13
14
  require_relative 'erp_integration/configuration'
14
15
 
15
16
  # Middleware
17
+ require_relative 'erp_integration/middleware/api_keys_rotation'
16
18
  require_relative 'erp_integration/middleware/error_handling'
19
+ require_relative 'erp_integration/middleware/logger'
17
20
 
18
21
  # HTTP clients
19
22
  require_relative 'erp_integration/fulfil/client'
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: erp_integration
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.52.0
4
+ version: 0.53.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Stefan Vermaas
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2024-09-12 00:00:00.000000000 Z
11
+ date: 2024-10-30 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activesupport
@@ -259,6 +259,7 @@ files:
259
259
  - bin/setup
260
260
  - erp_integration.gemspec
261
261
  - lib/erp_integration.rb
262
+ - lib/erp_integration/api_keys_pool.rb
262
263
  - lib/erp_integration/bill_of_material.rb
263
264
  - lib/erp_integration/bill_of_material_input.rb
264
265
  - lib/erp_integration/bill_of_material_output.rb
@@ -316,7 +317,9 @@ files:
316
317
  - lib/erp_integration/gift_card.rb
317
318
  - lib/erp_integration/internal_shipment.rb
318
319
  - lib/erp_integration/location.rb
320
+ - lib/erp_integration/middleware/api_keys_rotation.rb
319
321
  - lib/erp_integration/middleware/error_handling.rb
322
+ - lib/erp_integration/middleware/logger.rb
320
323
  - lib/erp_integration/party_address.rb
321
324
  - lib/erp_integration/product.rb
322
325
  - lib/erp_integration/product_category.rb
@@ -365,7 +368,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
365
368
  - !ruby/object:Gem::Version
366
369
  version: '0'
367
370
  requirements: []
368
- rubygems_version: 3.5.11
371
+ rubygems_version: 3.2.22
369
372
  signing_key:
370
373
  specification_version: 4
371
374
  summary: Connects Mejuri with third-party ERP vendors