eol-client 1.0.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.
Files changed (77) hide show
  1. checksums.yaml +7 -0
  2. data/.drone.yaml +12 -0
  3. data/.gitignore +16 -0
  4. data/.rspec +2 -0
  5. data/.rubocop.yml +60 -0
  6. data/.ruby-version +1 -0
  7. data/.travis.yml +11 -0
  8. data/Changelog.md +3 -0
  9. data/Gemfile +6 -0
  10. data/Guardfile +13 -0
  11. data/LICENSE +21 -0
  12. data/README.md +247 -0
  13. data/Rakefile +13 -0
  14. data/bin/console +16 -0
  15. data/bin/setup +7 -0
  16. data/eol.gemspec +40 -0
  17. data/jenkins.sh +33 -0
  18. data/lib/eol.rb +79 -0
  19. data/lib/eol/api.rb +32 -0
  20. data/lib/eol/client.rb +13 -0
  21. data/lib/eol/config.rb +105 -0
  22. data/lib/eol/exception.rb +21 -0
  23. data/lib/eol/oauth.rb +117 -0
  24. data/lib/eol/parser.rb +42 -0
  25. data/lib/eol/request.rb +63 -0
  26. data/lib/eol/resource.rb +104 -0
  27. data/lib/eol/resources/account.rb +43 -0
  28. data/lib/eol/resources/address.rb +36 -0
  29. data/lib/eol/resources/aging_receivables_list.rb +34 -0
  30. data/lib/eol/resources/bank_account.rb +32 -0
  31. data/lib/eol/resources/bank_entry.rb +22 -0
  32. data/lib/eol/resources/bank_entry_line.rb +20 -0
  33. data/lib/eol/resources/base_entry_line.rb +20 -0
  34. data/lib/eol/resources/cash_entry.rb +22 -0
  35. data/lib/eol/resources/cash_entry_line.rb +20 -0
  36. data/lib/eol/resources/contact.rb +27 -0
  37. data/lib/eol/resources/costcenter.rb +19 -0
  38. data/lib/eol/resources/costunit.rb +19 -0
  39. data/lib/eol/resources/division.rb +26 -0
  40. data/lib/eol/resources/document.rb +23 -0
  41. data/lib/eol/resources/document_attachment.rb +19 -0
  42. data/lib/eol/resources/general_journal_entry.rb +19 -0
  43. data/lib/eol/resources/general_journal_entry_line.rb +11 -0
  44. data/lib/eol/resources/gl_account.rb +27 -0
  45. data/lib/eol/resources/goods_delivery.rb +37 -0
  46. data/lib/eol/resources/goods_delivery_line.rb +38 -0
  47. data/lib/eol/resources/item.rb +36 -0
  48. data/lib/eol/resources/item_group.rb +23 -0
  49. data/lib/eol/resources/journal.rb +23 -0
  50. data/lib/eol/resources/layout.rb +26 -0
  51. data/lib/eol/resources/mailbox.rb +22 -0
  52. data/lib/eol/resources/payment_condition.rb +25 -0
  53. data/lib/eol/resources/printed_sales_invoice.rb +37 -0
  54. data/lib/eol/resources/project.rb +26 -0
  55. data/lib/eol/resources/purchase_entry.rb +20 -0
  56. data/lib/eol/resources/purchase_entry_line.rb +11 -0
  57. data/lib/eol/resources/receivables_list.rb +31 -0
  58. data/lib/eol/resources/sales_entry.rb +22 -0
  59. data/lib/eol/resources/sales_entry_line.rb +12 -0
  60. data/lib/eol/resources/sales_invoice.rb +25 -0
  61. data/lib/eol/resources/sales_invoice_line.rb +24 -0
  62. data/lib/eol/resources/sales_item_prices.rb +18 -0
  63. data/lib/eol/resources/sales_order.rb +23 -0
  64. data/lib/eol/resources/sales_order_line.rb +23 -0
  65. data/lib/eol/resources/shared_sales_attributes.rb +11 -0
  66. data/lib/eol/resources/time_transaction.rb +24 -0
  67. data/lib/eol/resources/transaction.rb +23 -0
  68. data/lib/eol/resources/transaction_line.rb +23 -0
  69. data/lib/eol/resources/user.rb +27 -0
  70. data/lib/eol/resources/vat_code.rb +20 -0
  71. data/lib/eol/response.rb +72 -0
  72. data/lib/eol/result_set.rb +62 -0
  73. data/lib/eol/sanitizer.rb +54 -0
  74. data/lib/eol/uri.rb +87 -0
  75. data/lib/eol/utils.rb +50 -0
  76. data/lib/eol/version.rb +15 -0
  77. metadata +356 -0
data/bin/setup ADDED
@@ -0,0 +1,7 @@
1
+ #!/bin/bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+
5
+ bundle install
6
+
7
+ # Do any other automated setup that you need to do here
data/eol.gemspec ADDED
@@ -0,0 +1,40 @@
1
+ # frozen_string_literal: true
2
+
3
+ lib = File.expand_path("../lib", __FILE__)
4
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
5
+ require "eol/version"
6
+
7
+ # rubocop:disable Metrics/BlockLength
8
+ Gem::Specification.new do |spec|
9
+ spec.name = "eol-client"
10
+ spec.authors = ["Ahmad Hassan"]
11
+ spec.email = ["er.ahmad.hassan@gmail.com"]
12
+
13
+ spec.summary = "API wrapper for Exact Online"
14
+ spec.homepage = "https://github.com/ahmadhasankhan/eol"
15
+ spec.licenses = %w[MIT]
16
+
17
+ spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(spec)/}) }
18
+ spec.require_paths = %w[lib]
19
+ spec.version = Eol::Version.to_s
20
+
21
+ spec.add_dependency "activeresource"
22
+ spec.add_dependency "activesupport"
23
+ spec.add_dependency "faraday", [">= 0.12.1"]
24
+ spec.add_dependency "mechanize", ">= 2.7.5"
25
+
26
+ spec.add_development_dependency "bundler"
27
+ spec.add_development_dependency "dotenv"
28
+ spec.add_development_dependency "guard-rspec"
29
+ spec.add_development_dependency "guard-rubocop"
30
+ spec.add_development_dependency "listen"
31
+ spec.add_development_dependency "mutant-rspec"
32
+ spec.add_development_dependency "rake"
33
+ spec.add_development_dependency "rspec"
34
+ spec.add_development_dependency "ruby_dep"
35
+ spec.add_development_dependency "rubycritic"
36
+ spec.add_development_dependency "simplecov"
37
+ spec.add_development_dependency "simplecov-rcov"
38
+ spec.add_development_dependency "webmock"
39
+ end
40
+ # rubocop:enable Metrics/BlockLength
data/jenkins.sh ADDED
@@ -0,0 +1,33 @@
1
+ #!/bin/bash
2
+ export RAILS_ENV=test
3
+ export COVERAGE=true
4
+
5
+ bundle install
6
+
7
+ if [[ -d coverage ]]; then
8
+ echo "Removing old coverage report"
9
+ rm -r coverage
10
+ fi
11
+
12
+ echo "--- Check style"
13
+
14
+ rubocop
15
+
16
+ if [[ $? -ne 0 ]]; then
17
+ echo "--- Style checks failed."
18
+ exit 1
19
+ fi
20
+
21
+ echo "--- Doing a static analysis"
22
+
23
+ rubycritic app lib config
24
+
25
+ echo "--- Running RSpec"
26
+
27
+ rspec --color spec --format progress --format html --out rspec.html
28
+ rspec=$?
29
+
30
+ if [[ $rspec -ne 0 ]]; then
31
+ echo "--- Some tests have failed."
32
+ exit 1
33
+ fi
data/lib/eol.rb ADDED
@@ -0,0 +1,79 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "eol/version"
4
+ require "eol/api"
5
+ require "eol/config"
6
+ require "eol/response"
7
+ require "eol/client"
8
+ require "eol/resource"
9
+ require "eol/result_set"
10
+ require "eol/sanitizer"
11
+ require "eol/resources/aging_receivables_list"
12
+ require "eol/resources/receivables_list"
13
+ require "eol/resources/shared_sales_attributes"
14
+ require "eol/resources/base_entry_line"
15
+ require "eol/resources/bank_entry"
16
+ require "eol/resources/bank_entry_line"
17
+ require "eol/resources/bank_account"
18
+ require "eol/resources/contact"
19
+ require "eol/resources/sales_invoice"
20
+ require "eol/resources/sales_invoice_line"
21
+ require "eol/resources/printed_sales_invoice"
22
+ require "eol/resources/layout"
23
+ require "eol/resources/journal"
24
+ require "eol/resources/sales_item_prices"
25
+ require "eol/resources/item"
26
+ require "eol/resources/item_group"
27
+ require "eol/resources/account"
28
+ require "eol/resources/address"
29
+ require "eol/resources/gl_account"
30
+ require "eol/resources/sales_entry"
31
+ require "eol/resources/sales_entry_line"
32
+ require "eol/resources/sales_order"
33
+ require "eol/resources/sales_order_line"
34
+ require "eol/resources/project"
35
+ require "eol/resources/time_transaction"
36
+ require "eol/resources/purchase_entry"
37
+ require "eol/resources/purchase_entry_line"
38
+ require "eol/resources/costunit"
39
+ require "eol/resources/costcenter"
40
+ require "eol/resources/transaction"
41
+ require "eol/resources/transaction_line"
42
+ require "eol/resources/document"
43
+ require "eol/resources/document_attachment"
44
+ require "eol/resources/mailbox"
45
+ require "eol/resources/vat_code"
46
+ require "eol/resources/general_journal_entry"
47
+ require "eol/resources/general_journal_entry_line"
48
+ require "eol/resources/payment_condition"
49
+ require "eol/resources/goods_delivery"
50
+ require "eol/resources/goods_delivery_line"
51
+ require "eol/resources/division"
52
+ require "eol/resources/user"
53
+
54
+ module Eol
55
+ extend Config
56
+
57
+ def self.client(options = {})
58
+ Eol::Client.new(options)
59
+ end
60
+
61
+ # Delegate to eol::Client
62
+ def self.method_missing(method, *args, &block)
63
+ super unless client.respond_to?(method)
64
+ client.send(method, *args, &block)
65
+ end
66
+
67
+ # Delegate to eol::Client
68
+ def self.respond_to?(method, include_all = false)
69
+ client.respond_to?(method, include_all) || super
70
+ end
71
+
72
+ def self.info(msg)
73
+ logger.info(msg)
74
+ end
75
+
76
+ def self.error(msg)
77
+ logger.error(msg)
78
+ end
79
+ end
data/lib/eol/api.rb ADDED
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ require File.expand_path("../request", __FILE__)
4
+ require File.expand_path("../config", __FILE__)
5
+ require File.expand_path("../oauth", __FILE__)
6
+
7
+ module Eol
8
+ # @private
9
+ class API
10
+ # @private
11
+ attr_accessor *Config::VALID_OPTIONS_KEYS
12
+
13
+ # Creates a new API
14
+ def initialize(options = {})
15
+ options = Eol.options.merge(options)
16
+ Config::VALID_OPTIONS_KEYS.each do |key|
17
+ send("#{key}=", options[key])
18
+ end
19
+ end
20
+
21
+ def config
22
+ conf = {}
23
+ Config::VALID_OPTIONS_KEYS.each do |key|
24
+ conf[key] = send key
25
+ end
26
+ conf
27
+ end
28
+
29
+ include Request
30
+ include OAuth
31
+ end
32
+ end
data/lib/eol/client.rb ADDED
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "faraday"
4
+
5
+ module Eol
6
+ class Client < API
7
+ def connection
8
+ Faraday.new do |faraday|
9
+ faraday.adapter Faraday.default_adapter
10
+ end
11
+ end
12
+ end
13
+ end
data/lib/eol/config.rb ADDED
@@ -0,0 +1,105 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "faraday"
4
+ require "active_resource/threadsafe_attributes"
5
+ require "logger"
6
+
7
+ module Eol
8
+ module Config
9
+ include ThreadsafeAttributes
10
+ # An array of valid keys in the options hash
11
+ VALID_OPTIONS_KEYS = %i(
12
+ access_token
13
+ adapter
14
+ client_id
15
+ client_secret
16
+ connection_options
17
+ redirect_uri
18
+ response_format
19
+ user_agent
20
+ endpoint
21
+ division
22
+ base_url
23
+ refresh_token
24
+ logger
25
+ ).freeze
26
+
27
+ # By default, don't set a user access token
28
+ DEFAULT_ACCESS_TOKEN = ""
29
+
30
+ DEFAULT_REFRESH_TOKEN = ""
31
+
32
+ # The adapter that will be used to connect if none is set
33
+ #
34
+ # @note The default faraday adapter is Net::HTTP.
35
+ DEFAULT_ADAPTER = Faraday.default_adapter
36
+
37
+ # By default, client id should be set in .env
38
+ DEFAULT_CLIENT_ID = ""
39
+
40
+ # By default, client secret should be set in .env
41
+ DEFAULT_CLIENT_SECRET = ""
42
+
43
+ # By default, don't set any connection options
44
+ DEFAULT_CONNECTION_OPTIONS = {}.freeze
45
+
46
+ DEFAULT_BASE_URL = "https://start.exactonline.nl"
47
+
48
+ # The endpoint that will be used to connect if none is set
49
+ DEFAULT_ENDPOINT = "api/v1"
50
+
51
+ # the division code you want to connect with
52
+ DEFAULT_DIVISION = ""
53
+
54
+ # The response format appended to the path and sent in the 'Accept' header if none is set
55
+ #
56
+ DEFAULT_FORMAT = :json
57
+
58
+ DEFAULT_REDIRECT_URI = "https://www.getpostman.com/oauth2/callback"
59
+
60
+ # By default, don't set user agent
61
+ DEFAULT_USER_AGENT = nil
62
+
63
+ # An array of valid request/response formats
64
+ VALID_FORMATS = [:json].freeze
65
+
66
+ DEFAULT_LOGGER = ::Logger.new(STDOUT)
67
+
68
+ # @private
69
+ threadsafe_attribute(*VALID_OPTIONS_KEYS)
70
+
71
+ # When this module is extended, set all configuration options to their default values
72
+ def self.extended(base)
73
+ base.reset
74
+ end
75
+
76
+ # Convenience method to allow configuration options to be set in a block
77
+ def configure
78
+ yield self
79
+ end
80
+
81
+ # Create a hash of options and their values
82
+ def options
83
+ VALID_OPTIONS_KEYS.inject({}) do |option, key|
84
+ option.merge!(key => send(key))
85
+ end
86
+ end
87
+
88
+ # Reset all configuration options to defaults
89
+ def reset
90
+ self.access_token = DEFAULT_ACCESS_TOKEN
91
+ self.adapter = DEFAULT_ADAPTER
92
+ self.client_id = DEFAULT_CLIENT_ID
93
+ self.client_secret = DEFAULT_CLIENT_SECRET
94
+ self.connection_options = DEFAULT_CONNECTION_OPTIONS
95
+ self.redirect_uri = DEFAULT_REDIRECT_URI
96
+ self.endpoint = DEFAULT_ENDPOINT
97
+ self.division = DEFAULT_DIVISION
98
+ self.base_url = DEFAULT_BASE_URL
99
+ self.response_format = DEFAULT_FORMAT
100
+ self.user_agent = DEFAULT_USER_AGENT
101
+ self.refresh_token = DEFAULT_REFRESH_TOKEN
102
+ self.logger = DEFAULT_LOGGER
103
+ end
104
+ end
105
+ end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Eol
4
+ class BadRequestException < StandardError
5
+ def initialize(response, parsed)
6
+ @response = response
7
+ @parsed = parsed
8
+ super(message)
9
+ end
10
+
11
+ def message
12
+ if @parsed.error_message.present?
13
+ "code #{@response.status}: #{@parsed.error_message}"
14
+ else
15
+ @response.inspect
16
+ end
17
+ end
18
+ end
19
+
20
+ class UnauthorizedException < StandardError; end
21
+ end
data/lib/eol/oauth.rb ADDED
@@ -0,0 +1,117 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "mechanize"
4
+ require "uri"
5
+ require "json"
6
+
7
+ require File.expand_path("../utils", __FILE__)
8
+ require File.expand_path("../response", __FILE__)
9
+
10
+ # from https://developers.exactonline.com/#Example retrieve access token.html
11
+
12
+ # This whole class is going to be replaced due to Exact Online's new policies.
13
+ # https://support.exactonline.com/community/s/knowledge-base#All-All-HNO-Concept-general-security-gen-auth-totpc
14
+
15
+ module Eol
16
+ # rubocop:disable Metrics/ModuleLength
17
+ module OAuth
18
+ def authorized?
19
+ # Do a test call, return false if 401 or any error code
20
+ response = Eol.get("/Current/Me", no_division: true)
21
+ response.results.first.present?
22
+ rescue BadRequestException
23
+ Eol.error "Not yet authorized"
24
+ return false
25
+ end
26
+
27
+ def authorize_division
28
+ get("/Current/Me", no_division: true).results.first.current_division
29
+ end
30
+
31
+ # Return URL for OAuth authorization
32
+ def authorize_url(options = {})
33
+ options[:response_type] ||= "code"
34
+ options[:redirect_uri] ||= redirect_uri
35
+ params = authorization_params.merge(options)
36
+ uri = URI("#{base_url}/api/oauth2/auth/")
37
+ uri.query = URI.encode_www_form(params)
38
+ uri.to_s
39
+ end
40
+
41
+ # Return an access token from authorization
42
+ def get_access_token(code, _options = {})
43
+ conn = Faraday.new(url: base_url) do |faraday|
44
+ faraday.request :url_encoded
45
+ faraday.adapter Faraday.default_adapter
46
+ end
47
+ params = access_token_params(code)
48
+ conn.post do |req|
49
+ req.url "/api/oauth2/token"
50
+ req.body = params
51
+ req.headers["Accept"] = "application/json"
52
+ end
53
+ end
54
+
55
+ # Return an access token from authorization via refresh token
56
+ def get_refresh_token(refresh_token)
57
+ conn = Faraday.new(url: config[:base_url]) do |faraday|
58
+ faraday.request :url_encoded
59
+ faraday.adapter Faraday.default_adapter
60
+ end
61
+
62
+ params = refresh_access_token_params(refresh_token)
63
+
64
+ conn.post do |req|
65
+ req.url "/api/oauth2/token"
66
+ req.body = params
67
+ req.headers["Accept"] = "application/json"
68
+ req.headers["Content-Type"] = "application/x-www-form-urlencoded"
69
+ end
70
+ end
71
+
72
+ private
73
+
74
+ def authorization_params
75
+ {
76
+ client_id: client_id
77
+ }
78
+ end
79
+
80
+ def access_token_params(code)
81
+ {
82
+ client_id: client_id,
83
+ client_secret: client_secret,
84
+ grant_type: "authorization_code",
85
+ code: code,
86
+ redirect_uri: redirect_uri
87
+ }
88
+ end
89
+
90
+ def refresh_access_token_params(code)
91
+ {
92
+ client_id: client_id,
93
+ client_secret: client_secret,
94
+ grant_type: "refresh_token",
95
+ refresh_token: code
96
+ }
97
+ end
98
+ end
99
+
100
+ class OauthResponse < Response
101
+ def body
102
+ JSON.parse(@response.body)
103
+ end
104
+
105
+ def access_token
106
+ body["access_token"]
107
+ end
108
+
109
+ def division
110
+ body["division"]
111
+ end
112
+
113
+ def refresh_token
114
+ body["refresh_token"]
115
+ end
116
+ end
117
+ end