business-central 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (32) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE.txt +21 -0
  3. data/README.md +120 -0
  4. data/lib/business_central.rb +22 -0
  5. data/lib/business_central/client.rb +94 -0
  6. data/lib/business_central/exceptions.rb +55 -0
  7. data/lib/business_central/object/account.rb +11 -0
  8. data/lib/business_central/object/base.rb +105 -0
  9. data/lib/business_central/object/company.rb +11 -0
  10. data/lib/business_central/object/helper.rb +15 -0
  11. data/lib/business_central/object/item.rb +50 -0
  12. data/lib/business_central/object/purchase_invoice.rb +77 -0
  13. data/lib/business_central/object/purchase_invoice_line.rb +41 -0
  14. data/lib/business_central/object/request.rb +80 -0
  15. data/lib/business_central/object/response.rb +60 -0
  16. data/lib/business_central/object/validation.rb +54 -0
  17. data/lib/business_central/object/vendor.rb +45 -0
  18. data/lib/business_central/version.rb +3 -0
  19. data/lib/core_ext/string.rb +25 -0
  20. data/test/business_central/client_test.rb +77 -0
  21. data/test/business_central/object/account_test.rb +48 -0
  22. data/test/business_central/object/company_test.rb +47 -0
  23. data/test/business_central/object/item_test.rb +128 -0
  24. data/test/business_central/object/purchase_invoice_line_test.rb +129 -0
  25. data/test/business_central/object/purchase_invoice_test.rb +125 -0
  26. data/test/business_central/object/request_test.rb +82 -0
  27. data/test/business_central/object/response_test.rb +26 -0
  28. data/test/business_central/object/validation_test.rb +61 -0
  29. data/test/business_central/object/vendor_test.rb +125 -0
  30. data/test/business_central_test.rb +7 -0
  31. data/test/test_helper.rb +11 -0
  32. metadata +190 -0
@@ -0,0 +1,15 @@
1
+ module BusinessCentral
2
+ module Object
3
+ module Helper
4
+ def object(object_name, *params)
5
+ define_method(object_name) do |argument=nil|
6
+ object = "@#{object_name}_cache".to_sym
7
+ if !instance_variable_defined?(object)
8
+ instance_variable_set(object, BusinessCentral::Object.const_get("#{object_name.to_s.to_camel_case(true)}".to_sym).new(self, argument))
9
+ end
10
+ instance_variable_get(object)
11
+ end
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,50 @@
1
+ module BusinessCentral
2
+ module Object
3
+ class Item < Base
4
+ OBJECT = 'items'.freeze
5
+
6
+ OBJECT_VALIDATION = {
7
+ number: {
8
+ maximum_length: 20
9
+ },
10
+ display_name: {
11
+ maximum_length: 100
12
+ },
13
+ type: {
14
+ required: true,
15
+ inclusive_of: [
16
+ 'Inventory',
17
+ 'Service',
18
+ 'Non-Inventory'
19
+ ]
20
+ },
21
+ item_category_code: {
22
+ maximum_length: 20
23
+ },
24
+ gtin: {
25
+ maximum_length: 14
26
+ },
27
+ tax_group_code: {
28
+ maximum_length: 20
29
+ }
30
+ }.freeze
31
+
32
+ OBJECT_METHODS = [
33
+ :get,
34
+ :post,
35
+ :patch,
36
+ :delete
37
+ ].freeze
38
+
39
+ def initialize(client, company_id:)
40
+ super(client, company_id: company_id)
41
+ @parent_path = [
42
+ {
43
+ path: 'companies',
44
+ id: company_id
45
+ }
46
+ ]
47
+ end
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,77 @@
1
+ module BusinessCentral
2
+ module Object
3
+ class PurchaseInvoice < Base
4
+ OBJECT = 'purchaseInvoices'.freeze
5
+
6
+ OBJECT_VALIDATION = {
7
+ number: {
8
+ maximum_length: 20
9
+ },
10
+ invoice_date: {
11
+ date: true
12
+ },
13
+ vendor_invoice_number: {
14
+ maximum_length: 35
15
+ },
16
+ vendor_number: {
17
+ maximum_length: 20
18
+ },
19
+ vendor_name: {
20
+ maximum_length: 50
21
+ },
22
+ currency_code: {
23
+ maximum_length: 10
24
+ },
25
+ status: {
26
+ maximum_length: 20,
27
+ inclusion_of: [
28
+ 'Draft',
29
+ 'In Review',
30
+ 'Open',
31
+ 'Paid',
32
+ 'Canceled',
33
+ 'Corrective. Read-Only'
34
+ ]
35
+ },
36
+ payment_terms: {
37
+ maximum_length: 10
38
+ },
39
+ shipment_method: {
40
+ maximum_length: 10
41
+ },
42
+ pay_to_name: {
43
+ maximum_length: 100
44
+ },
45
+ pay_to_contact: {
46
+ maximum_length: 100
47
+ },
48
+ pay_to_vendor_number: {
49
+ maximum_length: 20
50
+ },
51
+ ship_to_name: {
52
+ maximum_length: 100
53
+ },
54
+ ship_to_contact: {
55
+ maximum_length: 100
56
+ }
57
+ }.freeze
58
+
59
+ OBJECT_METHODS = [
60
+ :get,
61
+ :post,
62
+ :patch,
63
+ :delete
64
+ ].freeze
65
+
66
+ def initialize(client, company_id:)
67
+ super(client, company_id: company_id)
68
+ @parent_path = [
69
+ {
70
+ path: 'companies',
71
+ id: company_id
72
+ }
73
+ ]
74
+ end
75
+ end
76
+ end
77
+ end
@@ -0,0 +1,41 @@
1
+ module BusinessCentral
2
+ module Object
3
+ class PurchaseInvoiceLine < Base
4
+ OBJECT = 'purchaseInvoiceLines'.freeze
5
+
6
+ OBJECT_VALIDATION = {
7
+ line_type: {
8
+ inclusion_of: [
9
+ 'Comment',
10
+ 'Account',
11
+ 'Item',
12
+ 'Resource',
13
+ 'Fixed Asset',
14
+ 'Charge'
15
+ ]
16
+ }
17
+ }.freeze
18
+
19
+ OBJECT_METHODS = [
20
+ :get,
21
+ :post,
22
+ :patch,
23
+ :delete
24
+ ].freeze
25
+
26
+ def initialize(client, company_id:, purchase_invoice_id:)
27
+ super(client, company_id: company_id)
28
+ @parent_path = [
29
+ {
30
+ path: 'companies',
31
+ id: company_id
32
+ },
33
+ {
34
+ path: 'purchaseInvoices',
35
+ id: purchase_invoice_id
36
+ }
37
+ ]
38
+ end
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,80 @@
1
+ module BusinessCentral
2
+ module Object
3
+ class Request
4
+
5
+ def self.get(client, url)
6
+ request(:get, client, url)
7
+ end
8
+
9
+ def self.post(client, url, params)
10
+ request(:post, client, url, params: params)
11
+ end
12
+
13
+ def self.patch(client, url, etag, params)
14
+ request(:patch, client, url, etag: etag, params: params)
15
+ end
16
+
17
+ def self.delete(client, url, etag)
18
+ request(:delete, client, url, etag: etag)
19
+ end
20
+
21
+ private
22
+
23
+ def self.request(method, client, url, etag: '', params: {})
24
+ send do
25
+ uri = URI(url)
26
+ https = Net::HTTP.new(uri.host, uri.port);
27
+ https.use_ssl = true
28
+ request = Object.const_get("Net::HTTP::#{method.to_s.capitalize}").new(uri)
29
+ request['Content-Type'] = 'application/json'
30
+ request['Accept'] = 'application/json'
31
+ request['If-Match'] = etag if !etag.blank?
32
+ request.body = convert(params) if method == :post || method == :patch
33
+ if client.access_token
34
+ request['Authorization'] = "Bearer #{client.access_token.token}"
35
+ else
36
+ request.basic_auth(client.username, client.password)
37
+ end
38
+ https.request(request)
39
+ end
40
+ end
41
+
42
+ def self.convert(request = {})
43
+ result = {}
44
+ request.each do |key, value|
45
+ result[key.to_s.to_camel_case] = value
46
+ end
47
+
48
+ return result.to_json
49
+ end
50
+
51
+ def self.send
52
+ begin
53
+ request = yield
54
+ response = Response.new(request.read_body.to_s).results
55
+
56
+ if Response.success?(request.code.to_i)
57
+ return response
58
+ elsif Response.deleted?(request.code.to_i)
59
+ return true
60
+ else
61
+ if Response.unauthorized?(request.code.to_i)
62
+ raise UnauthorizedException.new
63
+ else
64
+ if response[:error][:code].present?
65
+ case response[:error][:code]
66
+ when 'Internal_CompanyNotFound'
67
+ raise CompanyNotFoundException.new
68
+ else
69
+ raise ApiException.new("#{request.code} - #{response[:error][:code]} #{response[:error][:message]}")
70
+ end
71
+ else
72
+ raise ApiException.new("#{request.code} - API call failed")
73
+ end
74
+ end
75
+ end
76
+ end
77
+ end
78
+ end
79
+ end
80
+ end
@@ -0,0 +1,60 @@
1
+ module BusinessCentral
2
+ module Object
3
+ class Response
4
+ attr_reader :results
5
+
6
+ def initialize(response)
7
+ @results = nil
8
+ if !response.blank?
9
+ @response = JSON.parse(response)
10
+ if @response.has_key?('value')
11
+ @response = @response['value']
12
+ end
13
+ process
14
+ end
15
+ end
16
+
17
+ def self.success?(status)
18
+ status == 200 || status == 201
19
+ end
20
+
21
+ def self.deleted?(status)
22
+ status == 204
23
+ end
24
+
25
+ def self.unauthorized?(status)
26
+ status == 401
27
+ end
28
+
29
+ private
30
+
31
+ def process
32
+ if @response.is_a?(Array)
33
+ @results = []
34
+ @response.each do |data|
35
+ @results << convert(data)
36
+ end
37
+ elsif @response.is_a?(Hash)
38
+ @results = convert(@response)
39
+ end
40
+ end
41
+
42
+ def convert(data)
43
+ result = {}
44
+ data.each do |key, value|
45
+ if key == "@odata.etag"
46
+ result[:etag] = value
47
+ elsif key == "@odata.context"
48
+ result[:context] = value
49
+ elsif value.is_a?(Hash)
50
+ result[key.to_snake_case.to_sym] = convert(value)
51
+ else
52
+ result[key.to_snake_case.to_sym] = value
53
+ end
54
+ end
55
+
56
+ return result
57
+ end
58
+ end
59
+ end
60
+ end
@@ -0,0 +1,54 @@
1
+ module BusinessCentral
2
+ module Object
3
+ class Validation
4
+ def initialize(validation_rules, object_params)
5
+ @validation_rules = validation_rules
6
+ @object_params = object_params
7
+ @errors = []
8
+ end
9
+
10
+ def valid?
11
+ @validation_rules.each do |rules_key, rules_value|
12
+ rules_value.each do |validation_key, validation_value|
13
+ if required?(validation_key, validation_value, @object_params[rules_key].to_s)
14
+ @errors << { field: rules_key, message: 'is a required field'}
15
+ end
16
+
17
+ if exceeds_maximum_length?(validation_key, validation_value, @object_params[rules_key].to_s)
18
+ @errors << { field: rules_key, message: "has exceeded the maximum length #{validation_value}"}
19
+ end
20
+
21
+ if not_inclusive_of?(validation_key, validation_value, @object_params[rules_key].to_s)
22
+ @errors << { field: rules_key, message: "is not one of #{validation_value.join(', ')}"}
23
+ end
24
+
25
+ if date_type?(validation_key, validation_value, @object_params[rules_key])
26
+ @errors << { field: rules_key, message: 'is not a date'}
27
+ end
28
+ end
29
+ end
30
+
31
+ raise InvalidObjectException.new(@errors) if @errors.any?
32
+ return true
33
+ end
34
+
35
+ private
36
+
37
+ def required?(validation_rule, validation_value, value)
38
+ validation_rule == :required && validation_value == true && value.blank?
39
+ end
40
+
41
+ def exceeds_maximum_length?(validation_rule, validation_value, value)
42
+ validation_rule == :maximum_length && value.length > validation_value
43
+ end
44
+
45
+ def not_inclusive_of?(validation_rule, validation_value, value)
46
+ validation_rule == :inclusion_of && !validation_value.include?(value) && !value.blank?
47
+ end
48
+
49
+ def date_type?(validation_rule, validation_value, value)
50
+ validation_rule == :date && validation_value == true && !value.is_a?(Date) && !value.nil?
51
+ end
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,45 @@
1
+ module BusinessCentral
2
+ module Object
3
+ class Vendor < Base
4
+ OBJECT = 'vendors'.freeze
5
+
6
+ OBJECT_VALIDATION = {
7
+ number: {
8
+ maximum_length: 20
9
+ },
10
+ display_name: {
11
+ maximum_length: 100
12
+ },
13
+ phone_number: {
14
+ maximum_length: 30
15
+ },
16
+ email: {
17
+ maximum_length: 80
18
+ },
19
+ website: {
20
+ maximum_length: 80
21
+ },
22
+ tax_registration_number: {
23
+ maximum_length: 20
24
+ }
25
+ }.freeze
26
+
27
+ OBJECT_METHODS = [
28
+ :get,
29
+ :post,
30
+ :patch,
31
+ :delete
32
+ ].freeze
33
+
34
+ def initialize(client, company_id:)
35
+ super(client, company_id: company_id)
36
+ @parent_path = [
37
+ {
38
+ path: 'companies',
39
+ id: company_id
40
+ }
41
+ ]
42
+ end
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,3 @@
1
+ module BusinessCentral
2
+ VERSION = "1.0.0".freeze
3
+ end
@@ -0,0 +1,25 @@
1
+ class String
2
+ def blank?
3
+ empty? || /\A[[:space:]]*\z/.match?(self)
4
+ end
5
+
6
+ # Convert string to CamelCase
7
+ def to_camel_case(uppercase_first_letter = false)
8
+ string = self
9
+ if uppercase_first_letter
10
+ string = string.sub(/^[a-z\d]*/) { |match| match.capitalize }
11
+ else
12
+ string = string.sub(/^(?:(?=\b|[A-Z_])|\w)/) { |match| match.downcase }
13
+ end
14
+ string.gsub(/(?:_|(\/))([a-z\d]*)/) { "#{$1}#{$2.capitalize}" }.gsub("/", "::")
15
+ end
16
+
17
+ # Convert string to snake_case
18
+ def to_snake_case
19
+ self.gsub(/::/, '/').
20
+ gsub(/([A-Z]+)([A-Z][a-z])/,'\1_\2').
21
+ gsub(/([a-z\d])([A-Z])/,'\1_\2').
22
+ tr("-", "_").
23
+ downcase
24
+ end
25
+ end