erp_integration 0.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.
data/bin/rake ADDED
@@ -0,0 +1,29 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ #
5
+ # This file was generated by Bundler.
6
+ #
7
+ # The application 'rake' is installed as part of a gem, and
8
+ # this file is here to facilitate running it.
9
+ #
10
+
11
+ require 'pathname'
12
+ ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../../Gemfile',
13
+ Pathname.new(__FILE__).realpath)
14
+
15
+ bundle_binstub = File.expand_path('bundle', __dir__)
16
+
17
+ if File.file?(bundle_binstub)
18
+ if File.read(bundle_binstub, 300) =~ /This file was generated by Bundler/
19
+ load(bundle_binstub)
20
+ else
21
+ abort("Your `bin/bundle` was not generated by Bundler, so this binstub cannot run.
22
+ Replace `bin/bundle` by running `bundle binstubs bundler --force`, then run this command again.")
23
+ end
24
+ end
25
+
26
+ require 'rubygems'
27
+ require 'bundler/setup'
28
+
29
+ load Gem.bin_path('rake', 'rake')
data/bin/reek ADDED
@@ -0,0 +1,29 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ #
5
+ # This file was generated by Bundler.
6
+ #
7
+ # The application 'reek' is installed as part of a gem, and
8
+ # this file is here to facilitate running it.
9
+ #
10
+
11
+ require 'pathname'
12
+ ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../../Gemfile',
13
+ Pathname.new(__FILE__).realpath)
14
+
15
+ bundle_binstub = File.expand_path('bundle', __dir__)
16
+
17
+ if File.file?(bundle_binstub)
18
+ if File.read(bundle_binstub, 300) =~ /This file was generated by Bundler/
19
+ load(bundle_binstub)
20
+ else
21
+ abort("Your `bin/bundle` was not generated by Bundler, so this binstub cannot run.
22
+ Replace `bin/bundle` by running `bundle binstubs bundler --force`, then run this command again.")
23
+ end
24
+ end
25
+
26
+ require 'rubygems'
27
+ require 'bundler/setup'
28
+
29
+ load Gem.bin_path('reek', 'reek')
data/bin/release ADDED
@@ -0,0 +1,7 @@
1
+ #!/usr/bin/env bash
2
+ VERSION=$1
3
+
4
+ # Build the new gem and publish it to Rubygems.
5
+ gem build erp_integration.gemspec
6
+ gem push "erp_integration-$VERSION.gem" --host https://rubygems.org
7
+ rm "erp_integration-$VERSION.gem"
data/bin/rspec ADDED
@@ -0,0 +1,29 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ #
5
+ # This file was generated by Bundler.
6
+ #
7
+ # The application 'rspec' is installed as part of a gem, and
8
+ # this file is here to facilitate running it.
9
+ #
10
+
11
+ require 'pathname'
12
+ ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../../Gemfile',
13
+ Pathname.new(__FILE__).realpath)
14
+
15
+ bundle_binstub = File.expand_path('bundle', __dir__)
16
+
17
+ if File.file?(bundle_binstub)
18
+ if File.read(bundle_binstub, 300) =~ /This file was generated by Bundler/
19
+ load(bundle_binstub)
20
+ else
21
+ abort("Your `bin/bundle` was not generated by Bundler, so this binstub cannot run.
22
+ Replace `bin/bundle` by running `bundle binstubs bundler --force`, then run this command again.")
23
+ end
24
+ end
25
+
26
+ require 'rubygems'
27
+ require 'bundler/setup'
28
+
29
+ load Gem.bin_path('rspec-core', 'rspec')
data/bin/rubocop ADDED
@@ -0,0 +1,29 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ #
5
+ # This file was generated by Bundler.
6
+ #
7
+ # The application 'rubocop' is installed as part of a gem, and
8
+ # this file is here to facilitate running it.
9
+ #
10
+
11
+ require 'pathname'
12
+ ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../../Gemfile',
13
+ Pathname.new(__FILE__).realpath)
14
+
15
+ bundle_binstub = File.expand_path('bundle', __dir__)
16
+
17
+ if File.file?(bundle_binstub)
18
+ if File.read(bundle_binstub, 300) =~ /This file was generated by Bundler/
19
+ load(bundle_binstub)
20
+ else
21
+ abort("Your `bin/bundle` was not generated by Bundler, so this binstub cannot run.
22
+ Replace `bin/bundle` by running `bundle binstubs bundler --force`, then run this command again.")
23
+ end
24
+ end
25
+
26
+ require 'rubygems'
27
+ require 'bundler/setup'
28
+
29
+ load Gem.bin_path('rubocop', 'rubocop')
data/bin/setup ADDED
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+ set -vx
5
+
6
+ bundle install
7
+
8
+ # Do any other automated setup that you need to do here
@@ -0,0 +1,54 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'lib/erp_integration/version'
4
+
5
+ Gem::Specification.new do |spec|
6
+ spec.name = 'erp_integration'
7
+ spec.version = ErpIntegration::VERSION
8
+ spec.authors = ['Stefan Vermaas']
9
+ spec.email = ['stefan@knowndecimal.com']
10
+
11
+ spec.summary = 'Connects Mejuri with third-party ERP vendors'
12
+ spec.license = 'MIT'
13
+ spec.homepage = 'https://www.github.com/mejuri-inc/erp-integration'
14
+ spec.required_ruby_version = Gem::Requirement.new('>= 2.3.7')
15
+
16
+ spec.metadata['allowed_push_host'] = 'https://rubygems.org'
17
+
18
+ spec.metadata['homepage_uri'] = spec.homepage
19
+ spec.metadata['source_code_uri'] = 'https://www.github.com/mejuri-inc/erp-integration'
20
+ spec.metadata['changelog_uri'] = 'https://www.github.com/mejuri-inc/erp-integration/blob/main/CHANGELOG.md'
21
+
22
+ # Specify which files should be added to the gem when it is released.
23
+ # The `git ls-files -z` loads the files in the RubyGem that have been added into git.
24
+ spec.files = Dir.chdir(File.expand_path(__dir__)) do
25
+ `git ls-files -z`.split("\x0").reject { |f| f.match(%r{\A(?:test|spec|features)/}) }
26
+ end
27
+ spec.bindir = 'exe'
28
+ spec.executables = spec.files.grep(%r{\Aexe/}) { |f| File.basename(f) }
29
+ spec.require_paths = ['lib']
30
+
31
+ # ActiveSupport is used for extended support of String inflections.
32
+ spec.add_dependency 'activesupport', '>= 0.12'
33
+
34
+ # Faraday is a HTTP/REST API client library to interact with third-party API vendors
35
+ spec.add_dependency 'faraday', '>= 0.17.3', '< 1.8.0'
36
+ spec.add_dependency 'faraday_middleware', '~> 0.14.0'
37
+
38
+ # Development dependencies
39
+ spec.add_development_dependency 'byebug', '~> 11.0'
40
+ spec.add_development_dependency 'flay', '~> 2.12', '>= 2.12.1'
41
+ spec.add_development_dependency 'github_changelog_generator', '~> 1.15.2'
42
+ spec.add_development_dependency 'rake', '~> 13.0'
43
+ spec.add_development_dependency 'reek', '< 6.0'
44
+ spec.add_development_dependency 'rspec', '~> 3.10'
45
+ spec.add_development_dependency 'rubocop', '< 0.82.0'
46
+ spec.add_development_dependency 'rubocop-rake', '~> 0.5'
47
+ spec.add_development_dependency 'rubocop-rspec', '< 1.39.0'
48
+ spec.add_development_dependency 'webmock', '~> 3.14.0'
49
+
50
+ # The `parallel` gem is a dev dependency for Rubocop. However, the versions
51
+ # for parallel after 1.19.2 don't work with ruby 2.3.x. As ruby 2.3.x is
52
+ # required for Mejuri, we're manually locking it to stay on `1.19.2`.
53
+ spec.add_development_dependency 'parallel', '1.19.2'
54
+ end
@@ -0,0 +1,73 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'faraday_middleware'
4
+
5
+ module ErpIntegration
6
+ module Clients
7
+ # The `FulfilClient` is a simple HTTP client configured to be used for API
8
+ # requests to the Fulfil endpoints.
9
+ class FulfilClient
10
+ attr_reader :api_key, :merchant_id
11
+ attr_writer :connection, :faraday_adapter
12
+
13
+ def initialize(api_key:, merchant_id:)
14
+ @api_key = api_key
15
+ @merchant_id = merchant_id
16
+ end
17
+
18
+ # Generates the url prefix for the Faraday connection client.
19
+ # @return [String] The base url for the Fulfil HTTP client
20
+ def base_url
21
+ "https://#{merchant_id}.fulfil.io/"
22
+ end
23
+
24
+ # Sets the default adapter for the Faraday Connection.
25
+ # @return [Symbol] The default Faraday adapter
26
+ def faraday_adapter
27
+ @faraday_adapter ||= Faraday.default_adapter
28
+ end
29
+
30
+ # Sets up the Faraday connection to talk to the Fulfil API.
31
+ # @return [Faraday::Connection] The configured Faraday connection
32
+ def connection
33
+ @connection ||= Faraday.new(url: base_url) do |faraday|
34
+ faraday.headers = default_headers
35
+
36
+ faraday.request :json # Encode request bodies as JSON
37
+ faraday.request :retry # Retry transient failures
38
+
39
+ faraday.response :follow_redirects
40
+ faraday.response :json # Decode response bodies as JSON
41
+
42
+ # Custom error handling for the error response
43
+ faraday.use ErpIntegration::Middleware::ErrorHandling
44
+
45
+ # Adapter definition should be last in order to make the json parsers be loaded correctly
46
+ faraday.adapter faraday_adapter
47
+ end
48
+ end
49
+
50
+ %i[delete get patch put post].each do |action_name|
51
+ define_method(action_name) do |path, options = {}|
52
+ connection.public_send(action_name, "api/#{version}/#{path}", options).body
53
+ end
54
+ end
55
+
56
+ # Sets the default version for the Fulfil API endpoints.
57
+ # @return [String] The Fulfil API version to be used
58
+ def version
59
+ @version ||= 'v2'
60
+ end
61
+
62
+ private
63
+
64
+ def default_headers
65
+ {
66
+ 'Accept': 'application/json',
67
+ 'Content-Type': 'application/json',
68
+ 'X-API-KEY': api_key
69
+ }
70
+ end
71
+ end
72
+ end
73
+ end
@@ -0,0 +1,65 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ErpIntegration
4
+ # Use the `Configuration` class to configure the ERP Integration gem. Use
5
+ # an initializer in your project configure the ERP Integration gem.
6
+ #
7
+ # @example
8
+ # ```ruby
9
+ # # config/initializers/erp_integration.rb
10
+ # ErpIntegration.configure do |config|
11
+ # config.fulfil_api_key = "..."
12
+ # end
13
+ # ```
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
19
+
20
+ # The `fulfil_merchant_id` is used by the `FulfilClient` to connect to
21
+ # the right Fulfil API endpoints.
22
+ # @return [String] The merchant ID for Fulfil.
23
+ attr_accessor :fulfil_merchant_id
24
+
25
+ # Allows configuring an adapter for the `Order` resource. When none is
26
+ # configured, it will default to Fulfil.
27
+ # @return [Symbol] The configured adapter for the orders
28
+ attr_writer :order_adapter
29
+
30
+ def initialize(**options)
31
+ options.each_pair do |key, value|
32
+ public_send("#{key}=", value) if respond_to?("#{key}=")
33
+ end
34
+ end
35
+
36
+ def order_adapter
37
+ @order_adapter || :fulfil
38
+ end
39
+ end
40
+
41
+ # Returns ERP Integration's configuration.
42
+ # @return [ErpIntegration::Configuration] ERP Integration's configuration
43
+ def self.config
44
+ @config ||= Configuration.new
45
+ end
46
+
47
+ # Allows setting a new configuration for the ERP Integration gem.
48
+ # @return [ErpIntegration::Configuration] ERP Integration's new configuration
49
+ def self.config=(configuration)
50
+ raise BadConfiguration unless configuration.is_a?(Configuration)
51
+
52
+ @config = configuration
53
+ end
54
+
55
+ # Allows modifying ERP Integration's configuration.
56
+ #
57
+ # @example
58
+ # ErpIntegration.configure do |config|
59
+ # config.some_api_key = "..."
60
+ # end
61
+ #
62
+ def self.configure
63
+ yield(config)
64
+ end
65
+ end
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ErpIntegration
4
+ # The `ErpIntegration::Error` is the default error class for any exception raised
5
+ # on purpose by this gem.
6
+ class Error < StandardError; end
7
+
8
+ # The `ErpIntegration::BadConfiguration` is raised whenever the newly provided
9
+ # configuration is not a valid configuration object.
10
+ class BadConfiguration < Error
11
+ def initialize
12
+ super(
13
+ '[ERP Integration] The provided configuration object is not a ' \
14
+ 'ErpIntegration::Configuration instance. Please provide a ' \
15
+ 'ErpIntegration::Configuration instance when overriding the configuration directly.'
16
+ )
17
+ end
18
+ end
19
+
20
+ class HttpError < Faraday::Error
21
+ class BadRequest < HttpError; end
22
+ class AuthorizationRequired < HttpError; end
23
+ class PaymentRequired < HttpError; end
24
+ class Forbidden < HttpError; end
25
+ class NotFound < HttpError; end
26
+ class MethodNotAllowed < HttpError; end
27
+ class NotAccepted < HttpError; end
28
+ class UnprocessableEntity < HttpError; end
29
+ class TooManyRequests < HttpError; end
30
+ class InternalServerError < HttpError; end
31
+ end
32
+ end
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ErpIntegration
4
+ module Middleware
5
+ # The `ErrorHandling` middleware allows to raise our own exceptions that
6
+ # are easier to catch for the host application (e.g. `mejuri-web`).
7
+ class ErrorHandling < Faraday::Response::Middleware
8
+ HTTP_ERROR_CODES = {
9
+ 400 => ErpIntegration::HttpError::BadRequest,
10
+ 401 => ErpIntegration::HttpError::AuthorizationRequired,
11
+ 402 => ErpIntegration::HttpError::PaymentRequired,
12
+ 403 => ErpIntegration::HttpError::Forbidden,
13
+ 404 => ErpIntegration::HttpError::NotFound,
14
+ 405 => ErpIntegration::HttpError::MethodNotAllowed,
15
+ 406 => ErpIntegration::HttpError::NotAccepted,
16
+ 422 => ErpIntegration::HttpError::UnprocessableEntity,
17
+ 429 => ErpIntegration::HttpError::TooManyRequests,
18
+ 500 => ErpIntegration::HttpError::InternalServerError
19
+ }.freeze
20
+
21
+ def on_complete(response)
22
+ key = response[:status].to_i
23
+ raise HTTP_ERROR_CODES[key], response_values(response) if HTTP_ERROR_CODES.key?(key)
24
+ end
25
+
26
+ def response_values(response)
27
+ { status: response.status, headers: response.response_headers, body: response.body }
28
+ end
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ErpIntegration
4
+ # The `Order` class exposes available ERP Order operations
5
+ # assigning them to the proper adapter
6
+ class Order < Resource
7
+ attr_accessor :id, :number
8
+
9
+ # Allows cancelling the entire sales order.
10
+ # @param id [Integer|String] The ID of the to be cancelled order.
11
+ # @return [boolean] Whether the sales order was cancelled successfully or not.
12
+ def self.cancel(id)
13
+ adapter.new.cancel(id)
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ErpIntegration
4
+ module Orders
5
+ # The `FulfilOrder` handles all interaction with sales orders in Fulfil.
6
+ class FulfilOrder
7
+ extend Forwardable
8
+ def_delegator 'ErpIntegration', :config
9
+
10
+ # Allows cancelling the entire sales order in Fulfil.
11
+ # @param id [Integer|String] The ID of the to be cancelled order.
12
+ # @return [boolean] Whether the sales order was cancelled successfully or not.
13
+ def cancel(id)
14
+ client.put("model/sale.sale/#{id}/cancel")
15
+ true
16
+
17
+ # Fulfil will return an 400 (a.k.a. "Bad Request") status code when a sales order couldn't
18
+ # be cancelled. If a sales order isn't cancellable by design, no exception should be raised.
19
+ #
20
+ # See the Fulfil's documentation for more information:
21
+ # https://developers.fulfil.io/rest_api/model/sale.sale/#cancel-a-sales-order
22
+ rescue ErpIntegration::HttpError::BadRequest
23
+ false
24
+ end
25
+
26
+ private
27
+
28
+ def client
29
+ @client ||= Clients::FulfilClient.new(
30
+ api_key: config.fulfil_api_key,
31
+ merchant_id: config.fulfil_merchant_id
32
+ )
33
+ end
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,78 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ErpIntegration
4
+ # The `ErpIntegration::Resource` is a generic, re-usable model for third-party sources.
5
+ #
6
+ # For the `ErpIntegration::Resource`, we're using the adapter pattern.
7
+ # Meaning; the `ErpIntegration::Resource` is a facade that delegates the
8
+ # actual work to an adapter.
9
+ #
10
+ # Every class that inherits from the `ErpIntegration::Resource`, will be able
11
+ # to configure a designated adapter. This allows configuring an adapter
12
+ # per resource to maximize the flexibility.
13
+ #
14
+ # @example
15
+ # ErpIntegration.configure do |config|
16
+ # config.order_adapter = :fulfil
17
+ # end
18
+ #
19
+ # $ ErpIntegration::Order.adapter
20
+ # => #<ErpIntegration::Orders::FulfilOrder />
21
+ #
22
+ # To add a new resource, follow these steps:
23
+ # 1. Add a new `attr_writer` in the `ErpIntegration::Configuration` class.
24
+ # 2. Add a new method to the `ErpIntegration::Configuration` that sets up the
25
+ # default adapter.
26
+ # 3. Create a new generic resource model in the `lib/erp_integration` folder.
27
+ # 4. Create a new pluralized folder name in the `lib/erp_integration` folder
28
+ # (e.g. `orders` for the `Order` resource).
29
+ # 5. Create a new adapter class prefixed with the adapter's name
30
+ # (e.g. `FulfilOrder` for the `Order` resource in the `lib/erp_integration/orders` folder).
31
+ class Resource
32
+ attr_accessor :raw_api_response
33
+
34
+ def initialize(attributes = {})
35
+ @raw_api_response = attributes
36
+
37
+ attributes.each_pair do |name, value|
38
+ public_send(:"#{name}=", value) if respond_to?(:"#{name}=")
39
+ end
40
+ end
41
+
42
+ class << self
43
+ # Dynamically defines and loads the adapter for the class inheriting from
44
+ # the `ErpIntegration::Resource`.
45
+ # @return [Class] The adapter class for the resource.
46
+ def adapter
47
+ return @adapter if defined?(@adapter)
48
+
49
+ require_relative File.join(File.dirname(__FILE__), '..', "#{adapter_path}.rb")
50
+ @adapter = adapter_klass.constantize
51
+ end
52
+
53
+ # Dynamically exposes the adapter class to the resource.
54
+ # @return [String] the adapter class as a string
55
+ def adapter_klass
56
+ "ErpIntegration::#{adapter_path.classify}"
57
+ end
58
+
59
+ # Retrieves the adapter type for the resource from the global configuration.
60
+ # @return [String] the adapter type for the resource
61
+ def adapter_type
62
+ ErpIntegration.config.send("#{resource_name}_adapter").to_s
63
+ end
64
+
65
+ # Provides a relative path to the adapter for the resource.
66
+ # @return [String] the path to the adapter
67
+ def adapter_path
68
+ "#{resource_name.pluralize}/#{adapter_type}_#{resource_name}"
69
+ end
70
+
71
+ # Derives the name of the resource from the class name.
72
+ # @return [String] the resource name
73
+ def resource_name
74
+ name.split('::').last.underscore
75
+ end
76
+ end
77
+ end
78
+ end
@@ -0,0 +1,3 @@
1
+ module ErpIntegration
2
+ VERSION = "0.2.0".freeze
3
+ end
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'active_support'
4
+ require 'active_support/core_ext/string/inflections'
5
+ require 'faraday'
6
+ require 'json'
7
+
8
+ require_relative 'erp_integration/version'
9
+ require_relative 'erp_integration/errors'
10
+ require_relative 'erp_integration/configuration'
11
+
12
+ # Middleware
13
+ require_relative 'erp_integration/middleware/error_handling'
14
+
15
+ # Resources
16
+ require_relative 'erp_integration/resource'
17
+ require_relative 'erp_integration/order'
18
+
19
+ # HTTP clients
20
+ require_relative 'erp_integration/clients/fulfil_client'
21
+
22
+ # The `ErpIntegration` integrates Mejuri with third-party ERP vendors.
23
+ module ErpIntegration
24
+ end