fortnox-api 0.8.2 → 0.9.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 (205) hide show
  1. checksums.yaml +4 -4
  2. data/.env.template +7 -0
  3. data/.env.test +11 -3
  4. data/.gitignore +7 -1
  5. data/.rubocop.yml +17 -1
  6. data/.travis.yml +10 -9
  7. data/CHANGELOG.md +22 -8
  8. data/CONTRIBUTE.md +21 -9
  9. data/DEVELOPER_README.md +72 -0
  10. data/Guardfile +13 -4
  11. data/README.md +226 -64
  12. data/Rakefile +128 -0
  13. data/bin/get_tokens +79 -0
  14. data/bin/renew_tokens +28 -0
  15. data/fortnox-api.gemspec +10 -9
  16. data/lib/fortnox/api/mappers/base/from_json.rb +4 -3
  17. data/lib/fortnox/api/mappers/base/to_json.rb +2 -3
  18. data/lib/fortnox/api/models/base.rb +12 -10
  19. data/lib/fortnox/api/models/customer.rb +55 -55
  20. data/lib/fortnox/api/models/label.rb +2 -2
  21. data/lib/fortnox/api/repositories/authentication.rb +61 -0
  22. data/lib/fortnox/api/repositories/base/savers.rb +3 -1
  23. data/lib/fortnox/api/repositories/base.rb +21 -35
  24. data/lib/fortnox/api/repositories.rb +1 -0
  25. data/lib/fortnox/api/request_handling.rb +30 -18
  26. data/lib/fortnox/api/types/document_row.rb +3 -3
  27. data/lib/fortnox/api/types/enums.rb +27 -11
  28. data/lib/fortnox/api/types/model.rb +1 -4
  29. data/lib/fortnox/api/types/sized.rb +2 -2
  30. data/lib/fortnox/api/types.rb +14 -1
  31. data/lib/fortnox/api/version.rb +1 -1
  32. data/lib/fortnox/api.rb +12 -32
  33. data/spec/fortnox/api/mappers/base/canonical_name_sym_spec.rb +4 -4
  34. data/spec/fortnox/api/mappers/base/from_json_spec.rb +10 -12
  35. data/spec/fortnox/api/mappers/base/to_json_spec.rb +48 -57
  36. data/spec/fortnox/api/mappers/base_spec.rb +4 -7
  37. data/spec/fortnox/api/mappers/contexts/json_conversion.rb +38 -33
  38. data/spec/fortnox/api/mappers/unit_spec.rb +3 -4
  39. data/spec/fortnox/api/models/base_spec.rb +27 -16
  40. data/spec/fortnox/api/models/unit_spec.rb +5 -3
  41. data/spec/fortnox/api/repositories/article_spec.rb +14 -9
  42. data/spec/fortnox/api/repositories/authentication_spec.rb +103 -0
  43. data/spec/fortnox/api/repositories/base_spec.rb +106 -319
  44. data/spec/fortnox/api/repositories/customer_spec.rb +37 -7
  45. data/spec/fortnox/api/repositories/examples/all.rb +0 -1
  46. data/spec/fortnox/api/repositories/examples/find.rb +5 -8
  47. data/spec/fortnox/api/repositories/examples/only.rb +4 -13
  48. data/spec/fortnox/api/repositories/examples/save.rb +32 -18
  49. data/spec/fortnox/api/repositories/examples/save_with_nested_model.rb +0 -5
  50. data/spec/fortnox/api/repositories/examples/save_with_specially_named_attribute.rb +1 -4
  51. data/spec/fortnox/api/repositories/examples/search.rb +4 -7
  52. data/spec/fortnox/api/repositories/invoice_spec.rb +64 -15
  53. data/spec/fortnox/api/repositories/order_spec.rb +11 -9
  54. data/spec/fortnox/api/repositories/project_spec.rb +7 -6
  55. data/spec/fortnox/api/repositories/terms_of_payment_spec.rb +9 -7
  56. data/spec/fortnox/api/repositories/unit_spec.rb +13 -11
  57. data/spec/fortnox/api/types/country_spec.rb +1 -1
  58. data/spec/fortnox/api/types/email_spec.rb +2 -2
  59. data/spec/fortnox/api/types/examples/document_row.rb +3 -3
  60. data/spec/fortnox/api/types/examples/enum.rb +4 -4
  61. data/spec/fortnox/api/types/examples/types.rb +1 -3
  62. data/spec/fortnox/api/types/housework_types_spec.rb +54 -61
  63. data/spec/fortnox/api/types/model_spec.rb +3 -27
  64. data/spec/fortnox/api/types/order_row_spec.rb +2 -2
  65. data/spec/fortnox/api/types/required_spec.rb +6 -11
  66. data/spec/fortnox/api/types/sales_account_spec.rb +57 -0
  67. data/spec/fortnox/api_spec.rb +19 -124
  68. data/spec/spec_helper.rb +0 -14
  69. data/spec/support/helpers/configuration_helper.rb +30 -3
  70. data/spec/support/helpers.rb +1 -1
  71. data/spec/support/matchers/type/attribute_matcher.rb +2 -2
  72. data/spec/support/matchers/type/have_nullable_date_matcher.rb +6 -4
  73. data/spec/support/matchers/type/have_nullable_matcher.rb +1 -1
  74. data/spec/support/matchers/type/have_nullable_string_matcher.rb +5 -5
  75. data/spec/support/matchers/type/require_attribute_matcher.rb +5 -5
  76. data/spec/support/matchers/type/type_matcher.rb +1 -1
  77. data/spec/support/vcr_setup.rb +16 -0
  78. data/spec/vcr_cassettes/articles/all.yml +16 -43
  79. data/spec/vcr_cassettes/articles/find_by_hash_failure.yml +10 -12
  80. data/spec/vcr_cassettes/articles/find_failure.yml +10 -12
  81. data/spec/vcr_cassettes/articles/find_id_1.yml +13 -14
  82. data/spec/vcr_cassettes/articles/find_new.yml +14 -16
  83. data/spec/vcr_cassettes/articles/multi_param_find_by_hash.yml +13 -15
  84. data/spec/vcr_cassettes/articles/save_new.yml +13 -15
  85. data/spec/vcr_cassettes/articles/save_old.yml +14 -16
  86. data/spec/vcr_cassettes/articles/save_with_specially_named_attribute.yml +13 -15
  87. data/spec/vcr_cassettes/articles/search_by_name.yml +16 -15
  88. data/spec/vcr_cassettes/articles/search_miss.yml +10 -12
  89. data/spec/vcr_cassettes/articles/search_with_special_char.yml +10 -12
  90. data/spec/vcr_cassettes/articles/single_param_find_by_hash.yml +13 -27
  91. data/spec/vcr_cassettes/authentication/expired_token.yml +54 -0
  92. data/spec/vcr_cassettes/authentication/invalid_authorization.yml +57 -0
  93. data/spec/vcr_cassettes/authentication/invalid_refresh_token.yml +58 -0
  94. data/spec/vcr_cassettes/authentication/valid_request.yml +63 -0
  95. data/spec/vcr_cassettes/customers/all.yml +20 -127
  96. data/spec/vcr_cassettes/customers/find_by_hash_failure.yml +10 -12
  97. data/spec/vcr_cassettes/customers/find_failure.yml +10 -12
  98. data/spec/vcr_cassettes/customers/find_id_1.yml +14 -15
  99. data/spec/vcr_cassettes/customers/find_new.yml +13 -15
  100. data/spec/vcr_cassettes/customers/find_with_sales_account.yml +63 -0
  101. data/spec/vcr_cassettes/customers/multi_param_find_by_hash.yml +13 -15
  102. data/spec/vcr_cassettes/customers/save_new.yml +12 -14
  103. data/spec/vcr_cassettes/customers/save_new_with_country_code_SE.yml +12 -14
  104. data/spec/vcr_cassettes/customers/save_new_with_sales_account.yml +63 -0
  105. data/spec/vcr_cassettes/customers/save_old.yml +13 -15
  106. data/spec/vcr_cassettes/customers/save_with_specially_named_attribute.yml +12 -14
  107. data/spec/vcr_cassettes/customers/search_by_name.yml +13 -45
  108. data/spec/vcr_cassettes/customers/search_miss.yml +10 -12
  109. data/spec/vcr_cassettes/customers/search_with_special_char.yml +10 -12
  110. data/spec/vcr_cassettes/customers/single_param_find_by_hash.yml +14 -16
  111. data/spec/vcr_cassettes/invoices/all.yml +47 -112
  112. data/spec/vcr_cassettes/invoices/filter_hit.yml +14 -18
  113. data/spec/vcr_cassettes/invoices/filter_invalid.yml +10 -12
  114. data/spec/vcr_cassettes/invoices/find_by_hash_failure.yml +10 -12
  115. data/spec/vcr_cassettes/invoices/find_failure.yml +10 -12
  116. data/spec/vcr_cassettes/invoices/find_id_1.yml +15 -16
  117. data/spec/vcr_cassettes/invoices/find_new.yml +16 -18
  118. data/spec/vcr_cassettes/invoices/multi_param_find_by_hash.yml +13 -15
  119. data/spec/vcr_cassettes/invoices/row_description_limit.yml +65 -0
  120. data/spec/vcr_cassettes/invoices/save_new.yml +14 -16
  121. data/spec/vcr_cassettes/invoices/save_new_with_comments.yml +14 -16
  122. data/spec/vcr_cassettes/invoices/save_new_with_country.yml +14 -15
  123. data/spec/vcr_cassettes/invoices/save_new_with_country_GB.yml +15 -16
  124. data/spec/vcr_cassettes/invoices/save_new_with_country_Norge.yml +14 -15
  125. data/spec/vcr_cassettes/invoices/save_new_with_country_Norway.yml +14 -15
  126. data/spec/vcr_cassettes/invoices/save_new_with_country_Sverige.yml +14 -15
  127. data/spec/vcr_cassettes/invoices/save_new_with_country_VA.yml +15 -16
  128. data/spec/vcr_cassettes/invoices/save_new_with_country_VI.yml +15 -16
  129. data/spec/vcr_cassettes/invoices/save_new_with_country_empty_string.yml +14 -15
  130. data/spec/vcr_cassettes/invoices/save_new_with_country_nil.yml +14 -15
  131. data/spec/vcr_cassettes/invoices/save_new_with_unsaved_parent.yml +65 -0
  132. data/spec/vcr_cassettes/invoices/save_old.yml +16 -18
  133. data/spec/vcr_cassettes/invoices/save_old_with_empty_comments.yml +16 -18
  134. data/spec/vcr_cassettes/invoices/save_old_with_empty_country.yml +16 -17
  135. data/spec/vcr_cassettes/invoices/save_old_with_nil_comments.yml +16 -18
  136. data/spec/vcr_cassettes/invoices/save_old_with_nil_country.yml +16 -17
  137. data/spec/vcr_cassettes/invoices/save_with_nested_model.yml +15 -16
  138. data/spec/vcr_cassettes/invoices/save_with_specially_named_attribute.yml +14 -15
  139. data/spec/vcr_cassettes/invoices/search_by_name.yml +13 -21
  140. data/spec/vcr_cassettes/invoices/search_miss.yml +10 -12
  141. data/spec/vcr_cassettes/invoices/search_with_special_char.yml +10 -12
  142. data/spec/vcr_cassettes/invoices/single_param_find_by_hash.yml +14 -16
  143. data/spec/vcr_cassettes/orders/all.yml +19 -113
  144. data/spec/vcr_cassettes/orders/filter_hit.yml +14 -20
  145. data/spec/vcr_cassettes/orders/filter_invalid.yml +10 -12
  146. data/spec/vcr_cassettes/orders/find_by_hash_failure.yml +10 -12
  147. data/spec/vcr_cassettes/orders/find_failure.yml +10 -12
  148. data/spec/vcr_cassettes/orders/find_id_1.yml +17 -17
  149. data/spec/vcr_cassettes/orders/find_new.yml +16 -18
  150. data/spec/vcr_cassettes/orders/housework_invalid_tax_reduction_type.yml +11 -13
  151. data/spec/vcr_cassettes/orders/housework_othercoses_invalid.yml +11 -13
  152. data/spec/vcr_cassettes/orders/housework_type_babysitting.yml +15 -16
  153. data/spec/vcr_cassettes/orders/housework_type_cleaning.yml +15 -16
  154. data/spec/vcr_cassettes/orders/housework_type_construction.yml +15 -16
  155. data/spec/vcr_cassettes/orders/housework_type_cooking.yml +11 -13
  156. data/spec/vcr_cassettes/orders/housework_type_electricity.yml +15 -16
  157. data/spec/vcr_cassettes/orders/housework_type_gardening.yml +15 -16
  158. data/spec/vcr_cassettes/orders/housework_type_glassmetalwork.yml +15 -16
  159. data/spec/vcr_cassettes/orders/housework_type_grounddrainagework.yml +15 -16
  160. data/spec/vcr_cassettes/orders/housework_type_hvac.yml +15 -16
  161. data/spec/vcr_cassettes/orders/housework_type_itservices.yml +15 -16
  162. data/spec/vcr_cassettes/orders/housework_type_majorappliancerepair.yml +15 -16
  163. data/spec/vcr_cassettes/orders/housework_type_masonry.yml +15 -16
  164. data/spec/vcr_cassettes/orders/housework_type_movingservices.yml +15 -16
  165. data/spec/vcr_cassettes/orders/housework_type_othercare.yml +15 -16
  166. data/spec/vcr_cassettes/orders/housework_type_othercosts.yml +15 -16
  167. data/spec/vcr_cassettes/orders/housework_type_paintingwallpapering.yml +15 -16
  168. data/spec/vcr_cassettes/orders/housework_type_snowplowing.yml +15 -16
  169. data/spec/vcr_cassettes/orders/housework_type_textileclothing.yml +15 -16
  170. data/spec/vcr_cassettes/orders/housework_type_tutoring.yml +11 -13
  171. data/spec/vcr_cassettes/orders/multi_param_find_by_hash.yml +13 -15
  172. data/spec/vcr_cassettes/orders/save_new.yml +16 -18
  173. data/spec/vcr_cassettes/orders/save_old.yml +16 -18
  174. data/spec/vcr_cassettes/orders/save_with_nested_model.yml +15 -16
  175. data/spec/vcr_cassettes/orders/search_by_name.yml +13 -17
  176. data/spec/vcr_cassettes/orders/search_miss.yml +10 -12
  177. data/spec/vcr_cassettes/orders/search_with_special_char.yml +10 -12
  178. data/spec/vcr_cassettes/orders/single_param_find_by_hash.yml +14 -16
  179. data/spec/vcr_cassettes/projects/all.yml +14 -37
  180. data/spec/vcr_cassettes/projects/find_by_hash_failure.yml +10 -12
  181. data/spec/vcr_cassettes/projects/find_failure.yml +10 -12
  182. data/spec/vcr_cassettes/projects/find_id_1.yml +13 -15
  183. data/spec/vcr_cassettes/projects/find_new.yml +14 -16
  184. data/spec/vcr_cassettes/projects/multi_param_find_by_hash.yml +15 -16
  185. data/spec/vcr_cassettes/projects/save_new.yml +13 -15
  186. data/spec/vcr_cassettes/projects/save_old.yml +14 -16
  187. data/spec/vcr_cassettes/projects/single_param_find_by_hash.yml +12 -14
  188. data/spec/vcr_cassettes/termsofpayments/all.yml +16 -23
  189. data/spec/vcr_cassettes/termsofpayments/find_failure.yml +10 -12
  190. data/spec/vcr_cassettes/termsofpayments/find_id_1.yml +13 -16
  191. data/spec/vcr_cassettes/termsofpayments/find_new.yml +12 -14
  192. data/spec/vcr_cassettes/termsofpayments/save_new.yml +12 -14
  193. data/spec/vcr_cassettes/termsofpayments/save_old.yml +12 -14
  194. data/spec/vcr_cassettes/units/all.yml +13 -24
  195. data/spec/vcr_cassettes/units/find_failure.yml +10 -12
  196. data/spec/vcr_cassettes/units/find_id_1.yml +13 -15
  197. data/spec/vcr_cassettes/units/find_new.yml +12 -14
  198. data/spec/vcr_cassettes/units/save_new.yml +12 -14
  199. data/spec/vcr_cassettes/units/save_old.yml +12 -14
  200. data/spec/vcr_cassettes/units/save_with_specially_named_attribute.yml +12 -14
  201. metadata +39 -230
  202. data/lib/fortnox/api/circular_queue.rb +0 -39
  203. data/spec/fortnox/api/circular_queue_spec.rb +0 -52
  204. data/spec/support/helpers/when_performing_helper.rb +0 -7
  205. data/temp.txt +0 -1
data/fortnox-api.gemspec CHANGED
@@ -1,7 +1,6 @@
1
-
2
1
  # frozen_string_literal: true
3
2
 
4
- lib = File.expand_path('../lib', __FILE__)
3
+ lib = File.expand_path('lib', __dir__)
5
4
  $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
6
5
  require_relative 'lib/fortnox/api/version'
7
6
 
@@ -11,16 +10,18 @@ Gem::Specification.new do |spec|
11
10
  spec.authors = ['Jonas Schubert Erlandsson', 'Hannes Elvemyr', 'Felix Holmgren', 'Mike Eirih']
12
11
  spec.email = ['info@accodeing.com']
13
12
  spec.summary = 'Gem to use Fortnox REST API in Ruby.'
14
- spec.description = 'This gem uses the HTTParty library to abstract away the REST calls. It gives you access to a '\
15
- 'number of objects that behave a lot like ActiveRecord instances, giving you access to methods '\
16
- 'like `all`, `find`, `find_by_...` and so on. And each individual instance can be easily'\
17
- 'persistable to Fortnox again using the `save` method.'
13
+ spec.description = 'This gem uses the HTTParty library to abstract away the REST calls. It gives you access to a ' \
14
+ 'number of objects that behave a lot like ActiveRecord instances, giving you access to ' \
15
+ 'methods like `all`, `find`, `find_by_...` and so on. And each individual instance can be' \
16
+ 'easily persistable to Fortnox again using the `save` method.'
18
17
  spec.homepage = 'http://github.com/accodeing/fortnox-api'
19
18
  spec.license = 'LGPL-3.0'
19
+ spec.metadata = {
20
+ 'rubygems_mfa_required' => 'true'
21
+ }
20
22
 
21
23
  spec.files = `git ls-files -z`.split("\x0")
22
24
  spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
23
- spec.test_files = spec.files.grep(%r{^(spec)/})
24
25
  spec.require_paths = ['lib']
25
26
 
26
27
  spec.required_ruby_version = '>= 2.7'
@@ -29,13 +30,14 @@ Gem::Specification.new do |spec|
29
30
  spec.add_dependency 'dry-container', '~> 0.10'
30
31
  spec.add_dependency 'dry-struct', '~> 1.6'
31
32
  spec.add_dependency 'dry-types', '~> 1.7'
32
- spec.add_dependency 'httparty', '~> 0.17'
33
+ spec.add_dependency 'httparty', '~> 0.21'
33
34
  spec.add_dependency 'jwt', '~> 2.3'
34
35
 
35
36
  spec.add_development_dependency 'bundler', '~> 2.4'
36
37
  spec.add_development_dependency 'dotenv', '~> 2.8'
37
38
  spec.add_development_dependency 'guard', '~> 2.18'
38
39
  spec.add_development_dependency 'guard-rspec', '~> 4.7'
40
+ spec.add_development_dependency 'guard-rubocop', '~> 1.5'
39
41
  spec.add_development_dependency 'pry', '~> 0'
40
42
  spec.add_development_dependency 'rake', '~> 13.0'
41
43
  spec.add_development_dependency 'rspec', '~> 3.12'
@@ -43,7 +45,6 @@ Gem::Specification.new do |spec|
43
45
  spec.add_development_dependency 'rubocop', '~> 1.39.0'
44
46
  spec.add_development_dependency 'rubocop-rspec', '~> 2.16.0'
45
47
  spec.add_development_dependency 'simplecov', '~> 0.22'
46
- spec.add_development_dependency 'timecop', '~> 0.9.6'
47
48
  spec.add_development_dependency 'vcr', '~> 6.1'
48
49
  spec.add_development_dependency 'webmock', '~> 3.18'
49
50
  end
@@ -4,7 +4,7 @@ module Fortnox
4
4
  module API
5
5
  module Mapper
6
6
  module FromJSON
7
- class MissingModelOrMapperException < StandardError
7
+ class MissingModelOrMapperException < Fortnox::API::Exception
8
8
  end
9
9
 
10
10
  def wrapped_json_collection_to_entities_hash(json_collection_hash)
@@ -45,10 +45,11 @@ module Fortnox
45
45
  # Raise exception during test run if this happens so that we can
46
46
  # add it before a new release.
47
47
 
48
- message = "for #{key} (#{mapper_name}, #{Fortnox::API::Mapper::DefaultTemplates.canonical_name_sym}) with"\
49
- " #{collection}"
48
+ message = "for #{key} (#{mapper_name}, #{Fortnox::API::Mapper::DefaultTemplates.canonical_name_sym}) " \
49
+ "with #{collection}"
50
50
 
51
51
  raise MissingModelOrMapperException, message if ENV['RUBY_ENV']
52
+
52
53
  Fortnox::API.logger.warn("Missing Model or Mapper implementation for #{key} with attributes: #{collection}")
53
54
  convert_hash_keys_from_json_format(collection, {})
54
55
  end
@@ -35,13 +35,12 @@ module Fortnox
35
35
  end
36
36
 
37
37
  def default_key_to_json_transform(key)
38
- key.to_s.split('_').map(&:capitalize).join('')
38
+ key.to_s.split('_').map(&:capitalize).join
39
39
  end
40
40
 
41
41
  def sanitise(hash, keys_to_filter)
42
42
  hash.reject do |key, value|
43
- next false if keys_to_filter.include?(key)
44
- value.nil?
43
+ keys_to_filter.include?(key) || value.nil?
45
44
  end
46
45
  end
47
46
  end
@@ -26,8 +26,8 @@ module Fortnox
26
26
  obj = preserve_meta_properties(hash) do
27
27
  super(hash)
28
28
  end
29
- rescue Dry::Struct::Error => e
30
- raise Fortnox::API::AttributeError, e
29
+ rescue Dry::Struct::Error => exception
30
+ raise Fortnox::API::AttributeError, exception
31
31
  end
32
32
 
33
33
  IceNine.deep_freeze(obj)
@@ -52,30 +52,30 @@ module Fortnox
52
52
  end
53
53
  end
54
54
 
55
- def to_hash(recursive = false)
55
+ def to_hash(recursive = false) # rubocop:disable Style/OptionalBooleanParameter
56
56
  return super() if recursive
57
57
 
58
58
  self.class.schema.each_with_object({}) do |key, result|
59
- # Only output attributes that have a value set
60
- result[key.name] = self[key.name] if self.send("#{key.name}?")
59
+ # Only output attributes that have a value set
60
+ result[key.name] = self[key.name] if send("#{key.name}?")
61
61
  end
62
62
  end
63
63
 
64
64
  def update(hash)
65
65
  old_attributes = to_hash
66
- new_attributes = old_attributes.merge(hash)
66
+ new_attributes = old_attributes.merge(hash).compact
67
67
 
68
68
  return self if new_attributes == old_attributes
69
69
 
70
- new_hash = new_attributes.delete_if { |_, value| value.nil? }
71
- new_hash[:new] = @new
72
- new_hash[:parent] = self
73
- self.class.new(new_hash)
70
+ new_attributes[:new] = @new
71
+ new_attributes[:parent] = self
72
+ self.class.new(new_attributes)
74
73
  end
75
74
 
76
75
  # Generic comparison, by value, use .eql? or .equal? for object identity.
77
76
  def ==(other)
78
77
  return false unless other.is_a? self.class
78
+
79
79
  to_hash == other.to_hash
80
80
  end
81
81
 
@@ -115,6 +115,8 @@ module Fortnox
115
115
  obj
116
116
  end
117
117
 
118
+ private_class_method :preserve_meta_properties
119
+
118
120
  private
119
121
 
120
122
  def private_attributes
@@ -30,31 +30,31 @@ module Fortnox
30
30
  # Url Direct URL to the record
31
31
  attribute :url, Types::Nullable::String.is(:read_only)
32
32
 
33
- # Address1 First address of the customer. 1024 characters
33
+ # Address1 First address of the customer. 1024 characters
34
34
  attribute :address1, Types::Sized::String[1024]
35
35
 
36
- # Address2 Second address of the customer. 1024 characters
36
+ # Address2 Second address of the customer. 1024 characters
37
37
  attribute :address2, Types::Sized::String[1024]
38
38
 
39
- # City City of the customer. 1024 characters
39
+ # City City of the customer. 1024 characters
40
40
  attribute :city, Types::Sized::String[1024]
41
41
 
42
- # Country Country of the customer. Read-only.
42
+ # Country Country of the customer. Read-only.
43
43
  attribute :country, Types::Nullable::String.is(:read_only)
44
44
 
45
- # Comments Comments. 1024 characters.
45
+ # Comments Comments. 1024 characters.
46
46
  attribute :comments, Types::Sized::String[1024]
47
47
 
48
- # Currency Currency of the customer, 3 letters
48
+ # Currency Currency of the customer, 3 letters
49
49
  attribute :currency, Types::Currency
50
50
 
51
- # CostCenter Cost center of the customer, Cost center in Fortnox
51
+ # CostCenter Cost center of the customer, Cost center in Fortnox
52
52
  attribute :cost_center, Types::Nullable::String
53
53
 
54
- # CountryCode Country code of the customer, 2 letters
54
+ # CountryCode Country code of the customer, 2 letters
55
55
  attribute :country_code, Types::CountryCode
56
56
 
57
- # CustomerNumber Customer number. 1024 characters
57
+ # CustomerNumber Customer number. 1024 characters
58
58
  attribute :customer_number, Types::Sized::String[1024]
59
59
 
60
60
  # DefaultDeliveryTypes The properties for this object is listed in the table for "Default Delivery Types".
@@ -63,146 +63,146 @@ module Fortnox
63
63
  # The properties for this object is listed in the table for "Default Templates".
64
64
  attribute :default_templates, Types::DefaultTemplates
65
65
 
66
- # DeliveryAddress1 First delivery address of the customer. 1024 characters
66
+ # DeliveryAddress1 First delivery address of the customer. 1024 characters
67
67
  attribute :delivery_address1, Types::Sized::String[1024]
68
68
 
69
- # DeliveryAddress2 Second delivery address of the customer. 1024 characters
69
+ # DeliveryAddress2 Second delivery address of the customer. 1024 characters
70
70
  attribute :delivery_address2, Types::Sized::String[1024]
71
71
 
72
- # DeliveryCity Delivery city of the customer. 1024 characters
72
+ # DeliveryCity Delivery city of the customer. 1024 characters
73
73
  attribute :delivery_city, Types::Sized::String[1024]
74
74
 
75
- # DeliveryCountry Delivery country of the customer. Read-only.
75
+ # DeliveryCountry Delivery country of the customer. Read-only.
76
76
  attribute :delivery_country, Types::Nullable::String.is(:read_only)
77
77
 
78
- # DeliveryCountryCode Delivery country code of the customer, 2 letters
78
+ # DeliveryCountryCode Delivery country code of the customer, 2 letters
79
79
  attribute :delivery_country_code, Types::CountryCode
80
80
 
81
- # DeliveryFax Delivery fax number of the customer. 1024 characters
81
+ # DeliveryFax Delivery fax number of the customer. 1024 characters
82
82
  attribute :delivery_fax, Types::Sized::String[1024]
83
83
 
84
- # DeliveryName Delivery name of the customer. 1024 characters
84
+ # DeliveryName Delivery name of the customer. 1024 characters
85
85
  attribute :delivery_name, Types::Sized::String[1024]
86
86
 
87
- # DeliveryPhone1 First delivery phone number of the customer. 1024 characters
87
+ # DeliveryPhone1 First delivery phone number of the customer. 1024 characters
88
88
  attribute :delivery_phone1, Types::Sized::String[1024]
89
89
 
90
- # DeliveryPhone2 Second delivery phone number of the customer. 1024 characters
90
+ # DeliveryPhone2 Second delivery phone number of the customer. 1024 characters
91
91
  attribute :delivery_phone2, Types::Sized::String[1024]
92
92
 
93
- # DeliveryZipCode Delivery zip code of the customer. 1024 characters.
93
+ # DeliveryZipCode Delivery zip code of the customer. 1024 characters.
94
94
  attribute :delivery_zip_code, Types::Sized::String[1024]
95
95
 
96
- # Email Email address of the customer. 1024 characters
96
+ # Email Email address of the customer. 1024 characters
97
97
  attribute :email, Types::Email
98
98
 
99
- # EmailInvoice Invoice email address of the customer. 1024 characters
99
+ # EmailInvoice Invoice email address of the customer. 1024 characters
100
100
  attribute :email_invoice, Types::Email
101
101
 
102
- # EmailInvoiceBCC Invoice BCC email address of the customer. 1024 characters
102
+ # EmailInvoiceBCC Invoice BCC email address of the customer. 1024 characters
103
103
  attribute :email_invoice_bcc, Types::Email
104
104
 
105
- # EmailInvoiceCC Invoice CC email address of the customer. 1024 characters
105
+ # EmailInvoiceCC Invoice CC email address of the customer. 1024 characters
106
106
  attribute :email_invoice_cc, Types::Email
107
107
 
108
- # EmailOffer Offer email address of the customer. 1024 characters
108
+ # EmailOffer Offer email address of the customer. 1024 characters
109
109
  attribute :email_offer, Types::Email
110
110
 
111
- # EmailOfferBCC Offer BCC email address of the customer. 1024 characters
111
+ # EmailOfferBCC Offer BCC email address of the customer. 1024 characters
112
112
  attribute :email_offer_bcc, Types::Email
113
113
 
114
- # EmailOfferCC Offer CC email address of the customer. 1024 characters
114
+ # EmailOfferCC Offer CC email address of the customer. 1024 characters
115
115
  attribute :email_offer_cc, Types::Email
116
116
 
117
- # EmailOrder Order email address of the customer. 1024 characters
117
+ # EmailOrder Order email address of the customer. 1024 characters
118
118
  attribute :email_order, Types::Email
119
119
 
120
- # EmailOrderBCC Order BCC email address of the customer. 1024 characters
120
+ # EmailOrderBCC Order BCC email address of the customer. 1024 characters
121
121
  attribute :email_order_bcc, Types::Email
122
122
 
123
- # EmailOrderCC Order CC email address of the customer. 1024 characters
123
+ # EmailOrderCC Order CC email address of the customer. 1024 characters
124
124
  attribute :email_order_cc, Types::Email
125
125
 
126
- # Fax Fax number of the customer. 1024 characters
126
+ # Fax Fax number of the customer. 1024 characters
127
127
  attribute :fax, Types::Sized::String[1024]
128
128
 
129
129
  # InvoiceAdministrationFee Invoice administration fee of the customer, 12 digits (incl. decimals).
130
130
  attribute :invoice_administration_fee,
131
131
  Types::Sized::Float[0.0, 99_999_999_999.9]
132
132
 
133
- # InvoiceDiscount Invoice discount of the customer, 12 digits (incl. decimals)
133
+ # InvoiceDiscount Invoice discount of the customer, 12 digits (incl. decimals)
134
134
  attribute :invoice_discount, Types::Sized::Float[0.0, 99_999_999_999.9]
135
135
 
136
- # InvoiceFreight Invoice freight fee of the customer, 12 digits (incl. decimals)
136
+ # InvoiceFreight Invoice freight fee of the customer, 12 digits (incl. decimals)
137
137
  attribute :invoice_freight, Types::Sized::Float[0.0, 99_999_999_999.9]
138
138
 
139
- # InvoiceRemark Invoice remark of the customer. 1024 characters
139
+ # InvoiceRemark Invoice remark of the customer. 1024 characters
140
140
  attribute :invoice_remark, Types::Sized::String[1024]
141
141
 
142
- # Name Name of the customer, 1024 characters
142
+ # Name Name of the customer, 1024 characters
143
143
  attribute :name, Types::Sized::String[1024].is(:required)
144
144
 
145
- # OrganisationNumber Organisation number of the customer. 30 characters
145
+ # OrganisationNumber Organisation number of the customer. 30 characters
146
146
  attribute :organisation_number, Types::Sized::String[30]
147
147
 
148
- # OurReference Our reference of the customer. 50 characters
148
+ # OurReference Our reference of the customer. 50 characters
149
149
  attribute :our_reference, Types::Sized::String[50]
150
150
 
151
- # Phone1 First phone number of the customer. 1024 characters
151
+ # Phone1 First phone number of the customer. 1024 characters
152
152
  attribute :phone1, Types::Sized::String[1024]
153
153
 
154
- # Phone2 Second phone number of the customer. 1024 characters
154
+ # Phone2 Second phone number of the customer. 1024 characters
155
155
  attribute :phone2, Types::Sized::String[1024]
156
156
 
157
- # PriceList Price list of the customer, Price list in Fortnox
157
+ # PriceList Price list of the customer, Price list in Fortnox
158
158
  attribute :price_list, Types::Nullable::String
159
159
 
160
- # Project Project of the customer, Project in Fortnox
160
+ # Project Project of the customer, Project in Fortnox
161
161
  attribute :project, Types::Nullable::String
162
162
 
163
- # SalesAccount Sales account of the customer, 4 digits
164
- attribute :sales_account, Types::AccountNumber
163
+ # SalesAccount Sales account of the customer, 4 characters
164
+ attribute :sales_account, Types::SalesAccount
165
165
 
166
- # ShowPriceVATIncluded Show prices with VAT included or not
166
+ # ShowPriceVATIncluded Show prices with VAT included or not
167
167
  attribute :show_price_vat_included, Types::Nullable::Boolean
168
168
 
169
- # TermsOfDeliveryCode Terms of delivery code of the customer
169
+ # TermsOfDeliveryCode Terms of delivery code of the customer
170
170
  attribute :terms_of_delivery, Types::Nullable::String
171
171
 
172
- # TermsOfPaymentCode Terms of payment code of the customer
172
+ # TermsOfPaymentCode Terms of payment code of the customer
173
173
  attribute :terms_of_payment, Types::Nullable::String
174
174
 
175
- # Type Customer type, PRIVATE / COMPANY
175
+ # Type Customer type, PRIVATE / COMPANY
176
176
  attribute :type, Types::CustomerType
177
177
 
178
- # VATNumber VAT number of the customer
178
+ # VATNumber VAT number of the customer
179
179
  attribute :vat_number, Types::Nullable::String
180
180
 
181
- # VATType VAT type of the customer, SEVAT / SEREVERSEDVAT / EUREVERSEDVAT / EUVAT / EXPORT
181
+ # VATType VAT type of the customer, SEVAT / SEREVERSEDVAT / EUREVERSEDVAT / EUVAT / EXPORT
182
182
  attribute :vat_type, Types::VATType
183
183
 
184
- # VisitAddress Visit address of the customer. 128 characters
184
+ # VisitAddress Visit address of the customer. 128 characters
185
185
  attribute :visiting_address, Types::Sized::String[128]
186
186
 
187
- # VisitCity Visit city of the customer. 128 characters
187
+ # VisitCity Visit city of the customer. 128 characters
188
188
  attribute :visiting_city, Types::Sized::String[128]
189
189
 
190
- # VisitCountry Visit country of the customer, read-only
190
+ # VisitCountry Visit country of the customer, read-only
191
191
  attribute :visiting_country, Types::Nullable::String.is(:read_only)
192
192
 
193
193
  # VisitingCountryCode Code of the visiting country for the customer, 2 letters
194
194
  attribute :visiting_country_code, Types::CountryCode
195
195
 
196
- # VisitZipCode Visit zip code of the customer. 10 characters
196
+ # VisitZipCode Visit zip code of the customer. 10 characters
197
197
  attribute :visiting_zip_code, Types::Sized::String[10]
198
198
 
199
- # WayOfDeliveryCode Way of delivery code of the customer
199
+ # WayOfDeliveryCode Way of delivery code of the customer
200
200
  attribute :way_of_delivery, Types::Nullable::String
201
201
 
202
- # YourReference Your reference of the customer. 50 characters
202
+ # YourReference Your reference of the customer. 50 characters
203
203
  attribute :your_reference, Types::Sized::String[50]
204
204
 
205
- # ZipCode Zip code of the customer. 10 characters
205
+ # ZipCode Zip code of the customer. 10 characters
206
206
  attribute :zip_code, Types::Sized::String[10]
207
207
  end
208
208
  end
@@ -8,10 +8,10 @@ module Fortnox
8
8
  class Label < Model::Base
9
9
  STUB = {}.freeze
10
10
 
11
- # Id integer, read-only. The ID of the label.
11
+ # Id integer, read-only. The ID of the label.
12
12
  attribute :id, Types::Required::Integer.is(:read_only)
13
13
 
14
- # Description string, 25 characters, required. Description of the label
14
+ # Description string, 25 characters, required. Description of the label
15
15
  attribute :description, Types::Sized::String[25].is(:read_only)
16
16
  end
17
17
  end
@@ -0,0 +1,61 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'base'
4
+
5
+ module Fortnox
6
+ module API
7
+ module Repository
8
+ class Authentication
9
+ def renew_tokens(refresh_token:, client_id:, client_secret:)
10
+ body = {
11
+ grant_type: 'refresh_token',
12
+ refresh_token: refresh_token
13
+ }
14
+
15
+ response = HTTParty.post(Fortnox::API.config.token_url,
16
+ headers: headers(client_id, client_secret),
17
+ body: body)
18
+
19
+ validate_response(response)
20
+
21
+ parsed_response_to_hash(response.parsed_response)
22
+ end
23
+
24
+ private
25
+
26
+ def parsed_response_to_hash(parsed_response)
27
+ {
28
+ access_token: parsed_response['access_token'],
29
+ refresh_token: parsed_response['refresh_token'],
30
+ expires_in: parsed_response['expires_in'],
31
+ token_type: parsed_response['token_type'],
32
+ scope: parsed_response['scope']
33
+ }
34
+ end
35
+
36
+ def headers(client_id, client_secret)
37
+ credentials = Base64.encode64("#{client_id}:#{client_secret}")
38
+
39
+ {
40
+ 'Content-type' => 'application/x-www-form-urlencoded',
41
+ Authorization: "Basic #{credentials}"
42
+ }
43
+ end
44
+
45
+ def validate_response(response)
46
+ return if response.code == 200
47
+
48
+ case response.code
49
+ when 400
50
+ raise Fortnox::API::RemoteServerError, "Bad request. Error: \"#{response.body}\""
51
+ when 401
52
+ raise Fortnox::API::RemoteServerError, "Unauthorized request. Error: \"#{response.body}\""
53
+ else
54
+ raise Exception, "Unable to renew access token. Response code: #{response.code}. " \
55
+ "Response message: #{response.message}. Response body: #{response.body}"
56
+ end
57
+ end
58
+ end
59
+ end
60
+ end
61
+ end
@@ -8,6 +8,7 @@ module Fortnox
8
8
  return true if entity.saved?
9
9
 
10
10
  return save_new(entity) if entity.new?
11
+
11
12
  update_existing(entity)
12
13
  end
13
14
 
@@ -33,8 +34,9 @@ module Fortnox
33
34
 
34
35
  def get_changes_on(entity)
35
36
  hash = @mapper.entity_to_hash(entity, @keys_filtered_on_save)
36
- parent_hash = @mapper.entity_to_hash(entity.parent, @keys_filtered_on_save)
37
+ return hash unless entity.parent.saved?
37
38
 
39
+ parent_hash = @mapper.entity_to_hash(entity.parent, @keys_filtered_on_save)
38
40
  @mapper.wrap_entity_json_hash(@mapper.diff(hash, parent_hash))
39
41
  end
40
42
 
@@ -1,6 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'httparty'
4
+ require 'jwt'
5
+ require 'base64'
4
6
 
5
7
  require_relative 'base/loaders'
6
8
  require_relative 'base/savers'
@@ -27,53 +29,47 @@ module Fortnox
27
29
  attr_accessor :headers
28
30
  attr_reader :mapper, :keys_filtered_on_save
29
31
 
30
- def self.set_headers(headers = {}) # rubocop:disable Naming/AccessorMethodName
32
+ def self.set_headers(headers = {})
31
33
  self.headers.merge!(headers)
32
34
  end
33
35
 
34
36
  HTTP_METHODS.each do |method|
35
37
  define_method method do |path, options = {}, &block|
36
38
  provided_headers = options[:headers] || {}
37
- provided_headers['Client-Secret'] = client_secret
38
- provided_headers['Access-Token'] = next_access_token
39
+ provided_headers['Authorization'] = "Bearer #{access_token}"
39
40
  options[:headers] = provided_headers
40
41
  options[:base_uri] ||= base_url
42
+
43
+ debug_log_request(path, options)
44
+
41
45
  execute do |remote|
42
46
  remote.send(method, path, options, &block)
43
47
  end
44
48
  end
45
49
  end
46
50
 
47
- def initialize(keys_filtered_on_save: [:url], token_store: :default)
51
+ def initialize(keys_filtered_on_save: [:url])
48
52
  @keys_filtered_on_save = keys_filtered_on_save
49
- @token_store = token_store
50
53
  @mapper = Registry[Mapper::Base.canonical_name_sym(self.class::MODEL)].new
51
- end
54
+ return unless access_token.nil?
52
55
 
53
- def next_access_token
54
- @access_tokens ||= CircularQueue.new(*access_tokens)
55
- @access_tokens.next
56
+ raise MissingAccessToken,
57
+ 'No Access Token provided! You need to provide an Access Token: ' \
58
+ 'Fortnox::API.access_token = token'
56
59
  end
57
60
 
58
- def check_access_tokens!(tokens)
59
- tokens_present = !(tokens.nil? || tokens.empty?)
60
- return if tokens_present
61
- error_message = "You have not provided any access tokens in token store #{@token_store.inspect}."
62
- raise MissingConfiguration, error_message
63
- end
61
+ private
64
62
 
65
- def access_tokens
66
- begin
67
- tokens = config.token_store.fetch(@token_store)
68
- rescue KeyError
69
- token_store_not_found!(@token_store.inspect)
70
- end
63
+ def debug_log_request(path, options)
64
+ return unless Fortnox::API.debugging
71
65
 
72
- check_access_tokens!(tokens)
73
- tokens
66
+ Fortnox::API.logger.debug("path: #{path.inspect}")
67
+ Fortnox::API.logger.debug("options: #{options.inspect}")
74
68
  end
75
69
 
76
- private
70
+ def access_token
71
+ Fortnox::API.access_token
72
+ end
77
73
 
78
74
  def instantiate(hash)
79
75
  hash[:new] = false
@@ -84,23 +80,13 @@ module Fortnox
84
80
  def base_url
85
81
  base_url = config.base_url
86
82
  raise MissingConfiguration, 'You have to provide a base url.' unless base_url
87
- base_url
88
- end
89
83
 
90
- def client_secret
91
- client_secret = config.client_secret
92
- raise MissingConfiguration, 'You have to provide your client secret.' unless client_secret
93
- client_secret
84
+ base_url
94
85
  end
95
86
 
96
87
  def config
97
88
  Fortnox::API.config
98
89
  end
99
-
100
- def token_store_not_found!(store_name)
101
- raise MissingConfiguration,
102
- "There is no token store named #{store_name}. Available stores are #{config.token_store.keys}."
103
- end
104
90
  end
105
91
  end
106
92
  end
@@ -7,3 +7,4 @@ require_relative 'repositories/order'
7
7
  require_relative 'repositories/project'
8
8
  require_relative 'repositories/unit'
9
9
  require_relative 'repositories/terms_of_payment'
10
+ require_relative 'repositories/authentication'
@@ -5,30 +5,42 @@ module Fortnox
5
5
  module RequestHandling
6
6
  private
7
7
 
8
- def raise_api_error(error, response)
9
- message = (error['message'] || error['Message'] || 'Okänt fel')
8
+ def raise_api_error(error, response)
9
+ message = (error['message'] || error['Message'] || 'Okänt fel')
10
10
 
11
- message += "\n\n#{response.request.inspect}" if Fortnox::API.debugging
11
+ message += "\n\n#{response.request.inspect}" if Fortnox::API.debugging
12
12
 
13
- raise Fortnox::API::RemoteServerError, message
14
- end
13
+ raise Fortnox::API::RemoteServerError, message
14
+ end
15
15
 
16
- def validate_response(response)
17
- return if response.code == 200
16
+ def validate_response(response)
17
+ return if response.code == 200
18
18
 
19
- api_error = response.parsed_response['ErrorInformation']
20
- raise_api_error(api_error, response) if api_error
21
- end
19
+ raise_content_type_error(response) if response.headers['content-type'].start_with?('text/html')
22
20
 
23
- def validate_and_parse(response)
24
- validate_response(response)
25
- response.parsed_response
26
- end
21
+ raise Fortnox::API::RemoteServerError, "Unauthorized request. Error: #{response.body}" if response.code == 401
22
+ raise Fortnox::API::RemoteServerError, "Forbidden request. Error: #{response.body}" if response.code == 403
27
23
 
28
- def execute
29
- response = yield(self.class)
30
- validate_and_parse response
31
- end
24
+ api_error = response.parsed_response['ErrorInformation']
25
+ raise_api_error(api_error, response) if api_error
26
+ end
27
+
28
+ def raise_content_type_error(response)
29
+ raise Fortnox::API::RemoteServerError,
30
+ "Fortnox API's response has content type \"text/html\" instead of requested \"application/json\"." \
31
+ 'This could be due to invalid endpoint or when the API is down. ' \
32
+ "Body: #{response.body}"
33
+ end
34
+
35
+ def validate_and_parse(response)
36
+ validate_response(response)
37
+ response.parsed_response
38
+ end
39
+
40
+ def execute
41
+ response = yield(self.class)
42
+ validate_and_parse response
43
+ end
32
44
  end
33
45
  end
34
46
  end