elmas 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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