elmas 0.1.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,19 @@
1
+ module Elmas
2
+ class BadRequestException < Exception
3
+ def initialize(object)
4
+ super(message)
5
+ @object = object
6
+ end
7
+
8
+ def message
9
+ case @object
10
+ when 500
11
+ "Server error"
12
+ else
13
+ "Something went wrong"
14
+ end
15
+ end
16
+ end
17
+
18
+ class UnauthorizedException < Exception; end
19
+ end
data/lib/elmas/log.rb ADDED
@@ -0,0 +1,17 @@
1
+ require "logger"
2
+
3
+ module Elmas
4
+ module Log
5
+ def logger
6
+ Logger.new("./tmp/errors.log", "daily")
7
+ end
8
+
9
+ def info(msg)
10
+ logger.info(msg)
11
+ end
12
+
13
+ def error(msg)
14
+ logger.error(msg)
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,109 @@
1
+ require "mechanize"
2
+ require "uri"
3
+
4
+ require File.expand_path("../utils", __FILE__)
5
+ require File.expand_path("../response", __FILE__)
6
+
7
+ # from https://developers.exactonline.com/#Example retrieve access token.html
8
+ module Elmas
9
+ module OAuth
10
+ def authorize(user_name, password, options = {})
11
+ agent = Mechanize.new
12
+
13
+ login(agent, user_name, password, options)
14
+ allow_access(agent)
15
+
16
+ code = URI.unescape(agent.page.uri.query.split("=").last)
17
+ OauthResponse.new(get_access_token(code))
18
+ end
19
+
20
+ def authorized?
21
+ response = get("/Current/Me", no_division: true)
22
+ !response.unauthorized?
23
+ # Do a test call, return false if 401 or any error code
24
+ end
25
+
26
+ def authorize_division
27
+ get("/Current/Me", no_division: true).first.current_division
28
+ end
29
+
30
+ # Return URL for OAuth authorization
31
+ def authorize_url(options = {})
32
+ options[:response_type] ||= "code"
33
+ options[:redirect_uri] ||= redirect_uri
34
+ params = authorization_params.merge(options)
35
+ uri = URI("https://start.exactonline.nl/api/oauth2/auth/")
36
+ uri.query = URI.encode_www_form(params)
37
+ uri.to_s
38
+ end
39
+
40
+ # Return an access token from authorization
41
+ def get_access_token(code, _options = {})
42
+ conn = Faraday.new(url: "https://start.exactonline.nl") do |faraday|
43
+ faraday.request :url_encoded
44
+ faraday.adapter Faraday.default_adapter
45
+ end
46
+ params = access_token_params(code)
47
+ conn.post do |req|
48
+ req.url "/api/oauth2/token"
49
+ req.body = params
50
+ req.headers["Accept"] = "application/json"
51
+ end
52
+ end
53
+
54
+ private
55
+
56
+ def login(agent, user_name, password, options)
57
+ # Login
58
+ agent.get(authorize_url(options)) do |page|
59
+ form = page.forms.first
60
+ form["UserNameField"] = user_name
61
+ form["PasswordField"] = password
62
+ form.click_button
63
+ end
64
+ end
65
+
66
+ def allow_access(agent)
67
+ return if agent.page.uri.to_s.include?("getpostman")
68
+ form = agent.page.form_with(id: "PublicOAuth2Form")
69
+ button = form.button_with(id: "AllowButton")
70
+ agent.submit(form, button)
71
+ end
72
+
73
+ def authorization_params
74
+ {
75
+ client_id: client_id
76
+ }
77
+ end
78
+
79
+ def access_token_params(code)
80
+ {
81
+ client_id: client_id,
82
+ client_secret: client_secret,
83
+ grant_type: "authorization_code",
84
+ code: code,
85
+ redirect_uri: redirect_uri
86
+ }
87
+ end
88
+ end
89
+ end
90
+
91
+ module Elmas
92
+ class OauthResponse < Response
93
+ def body
94
+ JSON.parse(@response.body)
95
+ end
96
+
97
+ def access_token
98
+ body["access_token"]
99
+ end
100
+
101
+ def division
102
+ body["division"]
103
+ end
104
+
105
+ def refresh_token
106
+ body["refresh_token"]
107
+ end
108
+ end
109
+ end
@@ -0,0 +1,15 @@
1
+ module Elmas
2
+ class Parser
3
+ def initialize(json)
4
+ @object = JSON.parse(json)
5
+ end
6
+
7
+ def results
8
+ @object["d"]["results"]
9
+ end
10
+
11
+ def first_result
12
+ results[0]
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,58 @@
1
+ module Elmas
2
+ # Defines HTTP request methods
3
+ module Request
4
+ # Perform an HTTP GET request
5
+ def get(path, options = {})
6
+ request(:get, path, options)
7
+ end
8
+
9
+ # Perform an HTTP POST request
10
+ def post(path, options = {})
11
+ request(:post, path, options)
12
+ end
13
+
14
+ # Perform an HTTP PUT request
15
+ def put(path, options = {})
16
+ request(:put, path, options)
17
+ end
18
+
19
+ # Perform an HTTP DELETE request
20
+ def delete(path, options = {})
21
+ request(:delete, path, options)
22
+ end
23
+
24
+ private
25
+
26
+ def build_path(path, options)
27
+ path = "#{division}/#{path}" unless options[:no_division]
28
+ path = "#{endpoint}/#{path}" unless options[:no_endpoint]
29
+ path = "#{options[:url] || base_url}/#{path}"
30
+ path
31
+ end
32
+
33
+ def add_headers
34
+ headers = {}
35
+ headers["Content-Type"] = "application/#{response_format}"
36
+ headers["Accept"] = "application/#{response_format}"
37
+ headers["Authorization"] = "Bearer #{access_token}" if access_token
38
+ headers
39
+ end
40
+
41
+ # Perform an HTTP request
42
+ def request(method, path, options = {})
43
+ path = build_path(path, options)
44
+
45
+ response = connection.send(method) do |request|
46
+ case method
47
+ when :post, :put
48
+ request.url path
49
+ request.body = options[:params].to_json
50
+ when :get, :delete
51
+ request.url path
52
+ end
53
+ request.headers = add_headers
54
+ end
55
+ Response.new(response)
56
+ end
57
+ end
58
+ end
@@ -0,0 +1,112 @@
1
+ require File.expand_path("../utils", __FILE__)
2
+ require File.expand_path("../exception", __FILE__)
3
+ require File.expand_path("../uri", __FILE__)
4
+
5
+ module Elmas
6
+ module Resource
7
+ include UriMethods
8
+
9
+ STANDARD_FILTERS = [:id].freeze
10
+
11
+ attr_accessor :attributes, :url
12
+ attr_reader :response
13
+
14
+ def initialize(attributes = {})
15
+ @attributes = Utils.normalize_hash(attributes)
16
+ @filters = STANDARD_FILTERS
17
+ @query = []
18
+ end
19
+
20
+ def find_all(options = {})
21
+ @order_by = options[:order_by]
22
+ @select = options[:select]
23
+ get(uri([:order, :select]))
24
+ end
25
+
26
+ # Pass filters in an array, for example 'filters: [:id, :name]'
27
+ def find_by(options = {})
28
+ @filters = options[:filters]
29
+ @order_by = options[:order_by]
30
+ @select = options[:select]
31
+ get(uri([:order, :select, :filters]))
32
+ end
33
+
34
+ def find
35
+ return nil unless id?
36
+ get(uri([:filters]))
37
+ end
38
+
39
+ # Normally use the url method (which applies the filters) but sometimes you only want to use the base path or other paths
40
+ def get(uri = self.uri)
41
+ @response = Elmas.get(URI.unescape(uri.to_s))
42
+ end
43
+
44
+ def valid?
45
+ valid = true
46
+ mandatory_attributes.each do |attribute|
47
+ valid = @attributes.key? attribute
48
+ end
49
+ valid
50
+ end
51
+
52
+ def id?
53
+ !@attributes[:id].nil?
54
+ end
55
+
56
+ def save
57
+ attributes_to_submit = sanitize
58
+ if valid?
59
+ if id?
60
+ return @response = Elmas.put(basic_identifier_uri, params: attributes_to_submit)
61
+ else
62
+ return @response = Elmas.post(base_path, params: attributes_to_submit)
63
+ end
64
+ else
65
+ Elmas.error("Invalid Resource #{self.class.name}, attributes: #{@attributes.inspect}")
66
+ Elmas::Response.new(Faraday::Response.new(status: 400, body: "Invalid Request"))
67
+ end
68
+ end
69
+
70
+ def delete
71
+ return nil unless id?
72
+ Elmas.delete(basic_identifier_uri)
73
+ end
74
+
75
+ # Parse the attributes for to post to the API
76
+ def sanitize
77
+ to_submit = {}
78
+ @attributes.each do |key, value|
79
+ next if key == :id || !valid_attribute?(key)
80
+ key = Utils.parse_key(key)
81
+ value.is_a?(Elmas::Resource) ? submit_value = value.id : submit_value = value # Turn relation into ID
82
+ to_submit[key] = submit_value
83
+ end
84
+ to_submit
85
+ end
86
+
87
+ # Getter/Setter for resource
88
+ def method_missing(method, *args, &block)
89
+ yield if block
90
+ if /^(\w+)=$/ =~ method
91
+ set_attribute($1, args[0])
92
+ else
93
+ nil unless @attributes[method.to_sym]
94
+ end
95
+ @attributes[method.to_sym]
96
+ end
97
+
98
+ private
99
+
100
+ def set_attribute(attribute, value)
101
+ @attributes[attribute.to_sym] = value if valid_attribute?(attribute)
102
+ end
103
+
104
+ def valid_attribute?(attribute)
105
+ valid_attributes.include?(attribute.to_sym)
106
+ end
107
+
108
+ def valid_attributes
109
+ @valid_attributes ||= mandatory_attributes.inject(other_attributes, :<<)
110
+ end
111
+ end
112
+ end
@@ -0,0 +1,40 @@
1
+ module Elmas
2
+ class Account
3
+ # An account needs a name
4
+ include Elmas::Resource
5
+
6
+ def base_path
7
+ "crm/Accounts"
8
+ end
9
+
10
+ def mandatory_attributes
11
+ [:name]
12
+ end
13
+
14
+ # https://start.exactonline.nl/docs/HlpRestAPIResourcesDetails.aspx?id=9
15
+ def other_attributes # rubocop:disable Metrics/MethodLength
16
+ [
17
+ :accountant, :account_manager, :activity_sector,
18
+ :activity_sub_sector, :address_line1, :address_line2,
19
+ :address_line3, :blocked, :business_type, :can_drop_ship,
20
+ :chamber_of_commerce, :city, :code, :code_at_supplier,
21
+ :company_size, :consolidation_scenario, :controlled_date,
22
+ :cost_paid, :country, :credit_line_purchase, :credit_line_sales,
23
+ :discount_purchase, :discount_sales, :email, :end_date, :fax,
24
+ :intra_stat_area, :intra_stat_delivery_term, :intra_stat_system,
25
+ :intra_stat_transaction_a, :intra_stat_transaction_b,
26
+ :intra_stat_transport_method, :invoice_acount, :invoice_attachment_type,
27
+ :invoicing_method, :is_accountant, :is_agency, :is_competitor, :is_mailing,
28
+ :is_pilot, :is_reseller, :is_sales, :is_supplier, :language, :latitude,
29
+ :lead_source, :logo, :logo_file_name, :longitude, :main_contact,
30
+ :payment_condition_purchase, :payment_condition_sales, :phone,
31
+ :phone_extension, :postcode, :price_list, :purchase_currency,
32
+ :purchase_lead_days, :purchase_VAT_code, :recipient_of_commissions,
33
+ :remarks, :reseller, :sales_currency, :sales_tax_schedule, :sales_vat_code,
34
+ :search_code, :security_level, :seperate_inv_per_project, :seperate_inv_per_subscription,
35
+ :shipping_lead_days, :shipping_method, :start_date, :state, :status,
36
+ :VAT_liability, :VAT_number, :website
37
+ ]
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,18 @@
1
+ module Elmas
2
+ class Contact
3
+ # A contact needs a First and Last name and a reference to an account
4
+ include Elmas::Resource
5
+
6
+ def base_path
7
+ "crm/Contacts"
8
+ end
9
+
10
+ def mandatory_attributes
11
+ [:first_name, :last_name, :account]
12
+ end
13
+
14
+ def other_attributes
15
+ []
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,19 @@
1
+ module Elmas
2
+ class Invoice
3
+ # An invoice usually has multiple invoice lines
4
+ # It should also have a journal id and a contact id who ordered it
5
+ include Elmas::Resource
6
+
7
+ def base_path
8
+ "salesinvoice/SalesInvoices"
9
+ end
10
+
11
+ def mandatory_attributes
12
+ [:journal, :ordered_by]
13
+ end
14
+
15
+ def other_attributes
16
+ [:sales_invoice_lines, :type]
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,18 @@
1
+ module Elmas
2
+ class InvoiceLine
3
+ # An invoice_line should always have a reference to an item and to an invoice.
4
+ include Elmas::Resource
5
+
6
+ def base_path
7
+ "salesinvoice/SalesInvoiceLines"
8
+ end
9
+
10
+ def mandatory_attributes
11
+ [:item]
12
+ end
13
+
14
+ def other_attributes
15
+ [:discount, :quantity, :amount_FC, :description, :vat_code]
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,17 @@
1
+ module Elmas
2
+ class Item
3
+ include Elmas::Resource
4
+
5
+ def base_path
6
+ "logistics/Items"
7
+ end
8
+
9
+ def other_attributes
10
+ []
11
+ end
12
+
13
+ def mandatory_attributes
14
+ [:code, :description]
15
+ end
16
+ end
17
+ end