fortnox-api 0.8.0 → 0.9.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (255) hide show
  1. checksums.yaml +4 -4
  2. data/.codeclimate.yml +1 -0
  3. data/.env.template +7 -0
  4. data/.env.test +11 -3
  5. data/.gitignore +7 -1
  6. data/.rubocop.yml +18 -2
  7. data/.tool-versions +1 -0
  8. data/.travis.yml +15 -12
  9. data/CHANGELOG.md +58 -3
  10. data/CONTRIBUTE.md +21 -9
  11. data/DEVELOPER_README.md +72 -0
  12. data/Guardfile +13 -4
  13. data/README.md +226 -64
  14. data/Rakefile +128 -0
  15. data/bin/get_tokens +79 -0
  16. data/bin/renew_tokens +28 -0
  17. data/fortnox-api.gemspec +31 -25
  18. data/lib/fortnox/api/mappers/article.rb +1 -1
  19. data/lib/fortnox/api/mappers/base/from_json.rb +5 -4
  20. data/lib/fortnox/api/mappers/base/to_json.rb +4 -5
  21. data/lib/fortnox/api/mappers/base.rb +3 -3
  22. data/lib/fortnox/api/mappers/customer.rb +1 -1
  23. data/lib/fortnox/api/mappers/default_delivery_types.rb +1 -1
  24. data/lib/fortnox/api/mappers/default_templates.rb +1 -1
  25. data/lib/fortnox/api/mappers/edi_information.rb +1 -1
  26. data/lib/fortnox/api/mappers/email_information.rb +1 -1
  27. data/lib/fortnox/api/mappers/invoice.rb +4 -4
  28. data/lib/fortnox/api/mappers/invoice_row.rb +1 -1
  29. data/lib/fortnox/api/mappers/order.rb +4 -4
  30. data/lib/fortnox/api/mappers/order_row.rb +1 -1
  31. data/lib/fortnox/api/mappers/project.rb +1 -1
  32. data/lib/fortnox/api/mappers/terms_of_payment.rb +1 -1
  33. data/lib/fortnox/api/mappers/unit.rb +1 -1
  34. data/lib/fortnox/api/mappers/value/country_string.rb +1 -1
  35. data/lib/fortnox/api/mappers.rb +18 -18
  36. data/lib/fortnox/api/models/article.rb +2 -2
  37. data/lib/fortnox/api/models/base.rb +23 -21
  38. data/lib/fortnox/api/models/customer.rb +57 -57
  39. data/lib/fortnox/api/models/document.rb +2 -2
  40. data/lib/fortnox/api/models/invoice.rb +2 -2
  41. data/lib/fortnox/api/models/label.rb +3 -3
  42. data/lib/fortnox/api/models/order.rb +2 -2
  43. data/lib/fortnox/api/models/project.rb +2 -2
  44. data/lib/fortnox/api/models/terms_of_payment.rb +2 -2
  45. data/lib/fortnox/api/models/unit.rb +2 -2
  46. data/lib/fortnox/api/models.rb +7 -7
  47. data/lib/fortnox/api/repositories/article.rb +3 -3
  48. data/lib/fortnox/api/repositories/authentication.rb +61 -0
  49. data/lib/fortnox/api/repositories/base/savers.rb +3 -1
  50. data/lib/fortnox/api/repositories/base.rb +25 -38
  51. data/lib/fortnox/api/repositories/customer.rb +3 -3
  52. data/lib/fortnox/api/repositories/invoice.rb +3 -3
  53. data/lib/fortnox/api/repositories/order.rb +3 -3
  54. data/lib/fortnox/api/repositories/project.rb +3 -3
  55. data/lib/fortnox/api/repositories/terms_of_payment.rb +3 -3
  56. data/lib/fortnox/api/repositories/unit.rb +3 -3
  57. data/lib/fortnox/api/repositories.rb +8 -7
  58. data/lib/fortnox/api/request_handling.rb +30 -18
  59. data/lib/fortnox/api/types/default_delivery_types.rb +0 -2
  60. data/lib/fortnox/api/types/default_templates.rb +0 -2
  61. data/lib/fortnox/api/types/document_row.rb +3 -3
  62. data/lib/fortnox/api/types/edi_information.rb +0 -2
  63. data/lib/fortnox/api/types/email_information.rb +0 -2
  64. data/lib/fortnox/api/types/enums.rb +27 -11
  65. data/lib/fortnox/api/types/invoice_row.rb +1 -1
  66. data/lib/fortnox/api/types/model.rb +5 -9
  67. data/lib/fortnox/api/types/nullable.rb +13 -9
  68. data/lib/fortnox/api/types/order_row.rb +1 -1
  69. data/lib/fortnox/api/types/required.rb +3 -3
  70. data/lib/fortnox/api/types/sized.rb +4 -4
  71. data/lib/fortnox/api/types.rb +31 -23
  72. data/lib/fortnox/api/version.rb +1 -1
  73. data/lib/fortnox/api.rb +21 -39
  74. data/spec/fortnox/api/mappers/base/canonical_name_sym_spec.rb +13 -11
  75. data/spec/fortnox/api/mappers/base/from_json_spec.rb +10 -12
  76. data/spec/fortnox/api/mappers/base/to_json_spec.rb +48 -57
  77. data/spec/fortnox/api/mappers/base_spec.rb +4 -7
  78. data/spec/fortnox/api/mappers/contexts/json_conversion.rb +38 -33
  79. data/spec/fortnox/api/mappers/default_delivery_types_spec.rb +1 -1
  80. data/spec/fortnox/api/mappers/examples/mapper.rb +1 -1
  81. data/spec/fortnox/api/mappers/unit_spec.rb +3 -4
  82. data/spec/fortnox/api/models/base_spec.rb +33 -22
  83. data/spec/fortnox/api/models/unit_spec.rb +5 -3
  84. data/spec/fortnox/api/repositories/article_spec.rb +14 -9
  85. data/spec/fortnox/api/repositories/authentication_spec.rb +103 -0
  86. data/spec/fortnox/api/repositories/base_spec.rb +105 -326
  87. data/spec/fortnox/api/repositories/customer_spec.rb +37 -7
  88. data/spec/fortnox/api/repositories/examples/all.rb +0 -1
  89. data/spec/fortnox/api/repositories/examples/find.rb +5 -8
  90. data/spec/fortnox/api/repositories/examples/only.rb +4 -13
  91. data/spec/fortnox/api/repositories/examples/save.rb +32 -18
  92. data/spec/fortnox/api/repositories/examples/save_with_nested_model.rb +0 -5
  93. data/spec/fortnox/api/repositories/examples/save_with_specially_named_attribute.rb +1 -4
  94. data/spec/fortnox/api/repositories/examples/search.rb +4 -7
  95. data/spec/fortnox/api/repositories/invoice_spec.rb +64 -21
  96. data/spec/fortnox/api/repositories/order_spec.rb +11 -9
  97. data/spec/fortnox/api/repositories/project_spec.rb +7 -6
  98. data/spec/fortnox/api/repositories/terms_of_payment_spec.rb +9 -7
  99. data/spec/fortnox/api/repositories/unit_spec.rb +13 -11
  100. data/spec/fortnox/api/types/country_spec.rb +1 -1
  101. data/spec/fortnox/api/types/email_spec.rb +2 -2
  102. data/spec/fortnox/api/types/examples/document_row.rb +3 -3
  103. data/spec/fortnox/api/types/examples/enum.rb +4 -4
  104. data/spec/fortnox/api/types/examples/types.rb +1 -3
  105. data/spec/fortnox/api/types/housework_types_spec.rb +54 -90
  106. data/spec/fortnox/api/types/model_spec.rb +13 -23
  107. data/spec/fortnox/api/types/nullable_spec.rb +30 -10
  108. data/spec/fortnox/api/types/order_row_spec.rb +2 -2
  109. data/spec/fortnox/api/types/required_spec.rb +7 -15
  110. data/spec/fortnox/api/types/sales_account_spec.rb +57 -0
  111. data/spec/fortnox/api_spec.rb +19 -124
  112. data/spec/spec_helper.rb +0 -14
  113. data/spec/support/helpers/configuration_helper.rb +30 -3
  114. data/spec/support/helpers.rb +1 -1
  115. data/spec/support/matchers/type/attribute_matcher.rb +2 -2
  116. data/spec/support/matchers/type/enum_matcher.rb +1 -1
  117. data/spec/support/matchers/type/have_account_number_matcher.rb +1 -1
  118. data/spec/support/matchers/type/have_email_matcher.rb +1 -1
  119. data/spec/support/matchers/type/have_nullable_date_matcher.rb +7 -5
  120. data/spec/support/matchers/type/have_nullable_matcher.rb +1 -1
  121. data/spec/support/matchers/type/have_nullable_string_matcher.rb +5 -5
  122. data/spec/support/matchers/type/require_attribute_matcher.rb +5 -5
  123. data/spec/support/matchers/type/type_matcher.rb +1 -1
  124. data/spec/support/vcr_setup.rb +16 -0
  125. data/spec/vcr_cassettes/articles/all.yml +32 -49
  126. data/spec/vcr_cassettes/articles/find_by_hash_failure.yml +27 -23
  127. data/spec/vcr_cassettes/articles/find_failure.yml +27 -23
  128. data/spec/vcr_cassettes/articles/find_id_1.yml +29 -24
  129. data/spec/vcr_cassettes/articles/find_new.yml +30 -26
  130. data/spec/vcr_cassettes/articles/multi_param_find_by_hash.yml +29 -25
  131. data/spec/vcr_cassettes/articles/save_new.yml +29 -25
  132. data/spec/vcr_cassettes/articles/save_old.yml +30 -26
  133. data/spec/vcr_cassettes/articles/save_with_specially_named_attribute.yml +29 -25
  134. data/spec/vcr_cassettes/articles/search_by_name.yml +32 -25
  135. data/spec/vcr_cassettes/articles/search_miss.yml +27 -23
  136. data/spec/vcr_cassettes/articles/search_with_special_char.yml +27 -23
  137. data/spec/vcr_cassettes/articles/single_param_find_by_hash.yml +29 -36
  138. data/spec/vcr_cassettes/authentication/expired_token.yml +54 -0
  139. data/spec/vcr_cassettes/authentication/invalid_authorization.yml +57 -0
  140. data/spec/vcr_cassettes/authentication/invalid_refresh_token.yml +58 -0
  141. data/spec/vcr_cassettes/authentication/valid_request.yml +63 -0
  142. data/spec/vcr_cassettes/customers/all.yml +35 -136
  143. data/spec/vcr_cassettes/customers/find_by_hash_failure.yml +27 -23
  144. data/spec/vcr_cassettes/customers/find_failure.yml +27 -23
  145. data/spec/vcr_cassettes/customers/find_id_1.yml +30 -25
  146. data/spec/vcr_cassettes/customers/find_new.yml +29 -25
  147. data/spec/vcr_cassettes/customers/find_with_sales_account.yml +63 -0
  148. data/spec/vcr_cassettes/customers/multi_param_find_by_hash.yml +29 -25
  149. data/spec/vcr_cassettes/customers/save_new.yml +28 -24
  150. data/spec/vcr_cassettes/customers/save_new_with_country_code_SE.yml +28 -24
  151. data/spec/vcr_cassettes/customers/save_new_with_sales_account.yml +63 -0
  152. data/spec/vcr_cassettes/customers/save_old.yml +29 -25
  153. data/spec/vcr_cassettes/customers/save_with_specially_named_attribute.yml +28 -24
  154. data/spec/vcr_cassettes/customers/search_by_name.yml +29 -53
  155. data/spec/vcr_cassettes/customers/search_miss.yml +27 -23
  156. data/spec/vcr_cassettes/customers/search_with_special_char.yml +27 -23
  157. data/spec/vcr_cassettes/customers/single_param_find_by_hash.yml +30 -26
  158. data/spec/vcr_cassettes/invoices/all.yml +62 -121
  159. data/spec/vcr_cassettes/invoices/filter_hit.yml +30 -28
  160. data/spec/vcr_cassettes/invoices/filter_invalid.yml +27 -23
  161. data/spec/vcr_cassettes/invoices/find_by_hash_failure.yml +27 -23
  162. data/spec/vcr_cassettes/invoices/find_failure.yml +27 -23
  163. data/spec/vcr_cassettes/invoices/find_id_1.yml +31 -26
  164. data/spec/vcr_cassettes/invoices/find_new.yml +32 -28
  165. data/spec/vcr_cassettes/invoices/multi_param_find_by_hash.yml +29 -25
  166. data/spec/vcr_cassettes/invoices/row_description_limit.yml +65 -0
  167. data/spec/vcr_cassettes/invoices/save_new.yml +31 -27
  168. data/spec/vcr_cassettes/invoices/save_new_with_comments.yml +31 -27
  169. data/spec/vcr_cassettes/invoices/save_new_with_country.yml +31 -26
  170. data/spec/vcr_cassettes/invoices/save_new_with_country_GB.yml +32 -27
  171. data/spec/vcr_cassettes/invoices/save_new_with_country_Norge.yml +31 -26
  172. data/spec/vcr_cassettes/invoices/save_new_with_country_Norway.yml +31 -26
  173. data/spec/vcr_cassettes/invoices/save_new_with_country_Sverige.yml +31 -26
  174. data/spec/vcr_cassettes/invoices/save_new_with_country_VA.yml +32 -27
  175. data/spec/vcr_cassettes/invoices/save_new_with_country_VI.yml +32 -27
  176. data/spec/vcr_cassettes/invoices/save_new_with_country_empty_string.yml +31 -26
  177. data/spec/vcr_cassettes/invoices/save_new_with_country_nil.yml +31 -26
  178. data/spec/vcr_cassettes/invoices/save_new_with_unsaved_parent.yml +65 -0
  179. data/spec/vcr_cassettes/invoices/save_old.yml +32 -28
  180. data/spec/vcr_cassettes/invoices/save_old_with_empty_comments.yml +32 -28
  181. data/spec/vcr_cassettes/invoices/save_old_with_empty_country.yml +32 -27
  182. data/spec/vcr_cassettes/invoices/save_old_with_nil_comments.yml +32 -28
  183. data/spec/vcr_cassettes/invoices/save_old_with_nil_country.yml +32 -27
  184. data/spec/vcr_cassettes/invoices/save_with_nested_model.yml +32 -27
  185. data/spec/vcr_cassettes/invoices/save_with_specially_named_attribute.yml +31 -26
  186. data/spec/vcr_cassettes/invoices/search_by_name.yml +29 -31
  187. data/spec/vcr_cassettes/invoices/search_miss.yml +27 -23
  188. data/spec/vcr_cassettes/invoices/search_with_special_char.yml +27 -23
  189. data/spec/vcr_cassettes/invoices/single_param_find_by_hash.yml +30 -26
  190. data/spec/vcr_cassettes/orders/all.yml +35 -123
  191. data/spec/vcr_cassettes/orders/filter_hit.yml +30 -30
  192. data/spec/vcr_cassettes/orders/filter_invalid.yml +27 -23
  193. data/spec/vcr_cassettes/orders/find_by_hash_failure.yml +27 -23
  194. data/spec/vcr_cassettes/orders/find_failure.yml +27 -23
  195. data/spec/vcr_cassettes/orders/find_id_1.yml +33 -27
  196. data/spec/vcr_cassettes/orders/find_new.yml +32 -28
  197. data/spec/vcr_cassettes/orders/housework_invalid_tax_reduction_type.yml +28 -24
  198. data/spec/vcr_cassettes/orders/housework_othercoses_invalid.yml +28 -24
  199. data/spec/vcr_cassettes/orders/housework_type_babysitting.yml +32 -27
  200. data/spec/vcr_cassettes/orders/housework_type_cleaning.yml +32 -27
  201. data/spec/vcr_cassettes/orders/housework_type_construction.yml +32 -27
  202. data/spec/vcr_cassettes/orders/housework_type_cooking.yml +28 -24
  203. data/spec/vcr_cassettes/orders/housework_type_electricity.yml +32 -27
  204. data/spec/vcr_cassettes/orders/housework_type_gardening.yml +32 -27
  205. data/spec/vcr_cassettes/orders/housework_type_glassmetalwork.yml +32 -27
  206. data/spec/vcr_cassettes/orders/housework_type_grounddrainagework.yml +32 -27
  207. data/spec/vcr_cassettes/orders/housework_type_hvac.yml +32 -27
  208. data/spec/vcr_cassettes/orders/housework_type_itservices.yml +32 -27
  209. data/spec/vcr_cassettes/orders/housework_type_majorappliancerepair.yml +32 -27
  210. data/spec/vcr_cassettes/orders/housework_type_masonry.yml +32 -27
  211. data/spec/vcr_cassettes/orders/housework_type_movingservices.yml +32 -27
  212. data/spec/vcr_cassettes/orders/housework_type_othercare.yml +32 -27
  213. data/spec/vcr_cassettes/orders/housework_type_othercosts.yml +32 -27
  214. data/spec/vcr_cassettes/orders/housework_type_paintingwallpapering.yml +32 -27
  215. data/spec/vcr_cassettes/orders/housework_type_snowplowing.yml +32 -27
  216. data/spec/vcr_cassettes/orders/housework_type_textileclothing.yml +32 -27
  217. data/spec/vcr_cassettes/orders/housework_type_tutoring.yml +28 -24
  218. data/spec/vcr_cassettes/orders/multi_param_find_by_hash.yml +29 -25
  219. data/spec/vcr_cassettes/orders/save_new.yml +32 -28
  220. data/spec/vcr_cassettes/orders/save_old.yml +32 -28
  221. data/spec/vcr_cassettes/orders/save_with_nested_model.yml +32 -27
  222. data/spec/vcr_cassettes/orders/search_by_name.yml +29 -27
  223. data/spec/vcr_cassettes/orders/search_miss.yml +27 -23
  224. data/spec/vcr_cassettes/orders/search_with_special_char.yml +27 -23
  225. data/spec/vcr_cassettes/orders/single_param_find_by_hash.yml +30 -26
  226. data/spec/vcr_cassettes/projects/all.yml +30 -43
  227. data/spec/vcr_cassettes/projects/find_by_hash_failure.yml +27 -23
  228. data/spec/vcr_cassettes/projects/find_failure.yml +27 -23
  229. data/spec/vcr_cassettes/projects/find_id_1.yml +29 -25
  230. data/spec/vcr_cassettes/projects/find_new.yml +30 -26
  231. data/spec/vcr_cassettes/projects/multi_param_find_by_hash.yml +31 -26
  232. data/spec/vcr_cassettes/projects/save_new.yml +29 -25
  233. data/spec/vcr_cassettes/projects/save_old.yml +30 -26
  234. data/spec/vcr_cassettes/projects/single_param_find_by_hash.yml +29 -25
  235. data/spec/vcr_cassettes/termsofpayments/all.yml +32 -32
  236. data/spec/vcr_cassettes/termsofpayments/find_failure.yml +27 -23
  237. data/spec/vcr_cassettes/termsofpayments/find_id_1.yml +29 -26
  238. data/spec/vcr_cassettes/termsofpayments/find_new.yml +29 -25
  239. data/spec/vcr_cassettes/termsofpayments/save_new.yml +29 -25
  240. data/spec/vcr_cassettes/termsofpayments/save_old.yml +29 -25
  241. data/spec/vcr_cassettes/units/all.yml +29 -32
  242. data/spec/vcr_cassettes/units/find_failure.yml +27 -23
  243. data/spec/vcr_cassettes/units/find_id_1.yml +29 -25
  244. data/spec/vcr_cassettes/units/find_new.yml +29 -25
  245. data/spec/vcr_cassettes/units/save_new.yml +29 -25
  246. data/spec/vcr_cassettes/units/save_old.yml +29 -25
  247. data/spec/vcr_cassettes/units/save_with_specially_named_attribute.yml +29 -25
  248. metadata +130 -261
  249. data/lib/fortnox/api/circular_queue.rb +0 -39
  250. data/spec/fortnox/api/circular_queue_spec.rb +0 -52
  251. data/spec/support/helpers/dummy_class_helper.rb +0 -38
  252. data/spec/support/helpers/when_performing_helper.rb +0 -7
  253. data/spec/vcr_cassettes/invoices/save_new_with_country_KR.yml +0 -61
  254. data/spec/vcr_cassettes/orders/housework_without_tax_reduction_type.yml +0 -57
  255. data/temp.txt +0 -1
@@ -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
@@ -1,7 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'fortnox/api/types'
4
-
5
3
  module Fortnox
6
4
  module API
7
5
  module Types
@@ -1,7 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'fortnox/api/types'
4
-
5
3
  module Fortnox
6
4
  module API
7
5
  module Types
@@ -22,8 +22,8 @@ module Fortnox
22
22
  # DeliveredQuantity Delivered quantity. 14 digits
23
23
  attribute :delivered_quantity, Types::Sized::Float[-9_999_999_999_999.9, 9_999_999_999_999.9]
24
24
 
25
- # Description Description Row description. 50 characters
26
- attribute :description, Types::Sized::String[50]
25
+ # Description Description Row description.
26
+ attribute :description, Types::Sized::String[255]
27
27
 
28
28
  # Discount amount. 12 digits (for amount) / 5 digits (for percent)
29
29
  # TODO(hannes): Verify that we can send in more than 5 digits through
@@ -45,7 +45,7 @@ module Fortnox
45
45
  attribute :housework_type, Types::HouseworkType
46
46
 
47
47
  # Price Price per unit. 12 digits
48
- attribute :price, Types::Sized::Float[0.0, 99_999_999_999.9]
48
+ attribute :price, Types::Sized::Float[-9_999_999_999, 99_999_999_999.9]
49
49
 
50
50
  # Project Code of the project for the row.
51
51
  attribute :project, Types::Nullable::String
@@ -1,7 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'fortnox/api/types'
4
-
5
3
  module Fortnox
6
4
  module API
7
5
  module Types
@@ -1,7 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'fortnox/api/types'
4
-
5
3
  module Fortnox
6
4
  module API
7
5
  module Types
@@ -7,6 +7,7 @@ module Fortnox
7
7
  def self.sized(size)
8
8
  lambda do |value|
9
9
  return nil if value == ''
10
+
10
11
  value.to_s.upcase[0...size] unless value.nil?
11
12
  end
12
13
  end
@@ -14,14 +15,16 @@ module Fortnox
14
15
  def self.default
15
16
  lambda do |value|
16
17
  return nil if value == ''
17
- value.to_s.upcase unless value.nil?
18
+
19
+ value&.to_s&.upcase
18
20
  end
19
21
  end
20
22
 
21
23
  def self.lower_case
22
24
  lambda do |value|
23
25
  return nil if value == ''
24
- value.to_s.downcase unless value.nil?
26
+
27
+ value&.to_s&.downcase
25
28
  end
26
29
  end
27
30
  end
@@ -33,16 +36,29 @@ module Fortnox
33
36
  'AMOUNT', 'PERCENT'
34
37
  )
35
38
  HOUSEWORK_TYPES = {
36
- rot: %w[
37
- CONSTRUCTION ELECTRICITY GLASSMETALWORK GROUNDDRAINAGEWORK
38
- MASONRY PAINTINGWALLPAPERING HVAC OTHERCOSTS
39
+ rot: [
40
+ 'CONSTRUCTION',
41
+ 'ELECTRICITY',
42
+ 'GLASSMETALWORK',
43
+ 'GROUNDDRAINAGEWORK',
44
+ 'HVAC',
45
+ 'MASONRY',
46
+ 'OTHERCOSTS',
47
+ 'PAINTINGWALLPAPERING'
39
48
  ],
40
- rut: %w[
41
- MAJORAPPLIANCEREPAIR MOVINGSERVICES ITSERVICES CLEANING
42
- TEXTILECLOTHING SNOWPLOWING GARDENING BABYSITTING OTHERCARE
43
- OTHERCOSTS
49
+ rut: [
50
+ 'BABYSITTING',
51
+ 'CLEANING',
52
+ 'GARDENING',
53
+ 'ITSERVICES',
54
+ 'MAJORAPPLIANCEREPAIR',
55
+ 'MOVINGSERVICES',
56
+ 'OTHERCARE',
57
+ 'OTHERCOSTS',
58
+ 'SNOWPLOWING',
59
+ 'TEXTILECLOTHING'
44
60
  ],
45
- legacy_rut: %w[COOKING TUTORING]
61
+ legacy_rut: ['COOKING', 'TUTORING']
46
62
  }.freeze
47
63
 
48
64
  # TODO: RUT to be added:
@@ -85,7 +101,7 @@ module Fortnox
85
101
  'SEVAT', 'SEREVERSEDVAT', 'EUREVERSEDVAT', 'EUVAT', 'EXPORT'
86
102
  )
87
103
  DefaultDeliveryTypeValues = Types::Strict::String.enum(
88
- 'PRINT', 'EMAIL', 'PRINTSERVICE'
104
+ 'PRINT', 'EMAIL', 'PRINTSERVICE', 'ELECTRONICINVOICE'
89
105
  )
90
106
  ProjectStatusTypes = Types::Strict::String.enum(
91
107
  'NOTSTARTED', 'ONGOING', 'COMPLETED'
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'fortnox/api/types/document_row'
3
+ require_relative 'document_row'
4
4
 
5
5
  module Fortnox
6
6
  module API
@@ -4,7 +4,7 @@ module Fortnox
4
4
  module API
5
5
  module Types
6
6
  class Model < Dry::Struct
7
- constructor_type(:schema)
7
+ transform_types(&:omittable)
8
8
 
9
9
  def initialize(input_attributes)
10
10
  if (missing_key = first_missing_required_key(input_attributes))
@@ -15,23 +15,19 @@ module Fortnox
15
15
  super
16
16
  end
17
17
 
18
- def self.is?(*_args) end
19
-
20
18
  private
21
19
 
22
20
  def missing_keys(attributes)
23
- non_nil_attributes = attributes.reject { |_, value| value.nil? }
24
-
25
- attribute_keys = non_nil_attributes.keys
26
- schema_keys = self.class.schema.keys
21
+ attribute_keys = attributes.compact.keys
22
+ schema_keys = self.class.schema.keys.map(&:name)
27
23
 
28
24
  schema_keys - attribute_keys
29
25
  end
30
26
 
31
27
  def first_missing_required_key(attributes)
32
28
  missing_keys(attributes).find do |name|
33
- attribute = self.class.schema[name]
34
- attribute.respond_to?(:options) && attribute.options[:required]
29
+ attribute = self.class.schema.keys.find { |key| key.name == name }
30
+ attribute.type.respond_to?(:options) && attribute.type.options[:required]
35
31
  end
36
32
  end
37
33
  end
@@ -6,15 +6,19 @@ module Fortnox
6
6
  module API
7
7
  module Types
8
8
  module Nullable
9
- String = Types::Strict::String.optional.constructor { |value| value.to_s unless value.nil? }
10
- Float = Types::Strict::Float.optional.constructor { |value| value.to_f unless value.nil? }
11
- Integer = Types::Strict::Int.optional.constructor { |value| value.to_i unless value.nil? }
12
- Boolean = Types::Bool.optional.constructor { |value| THE_TRUTH.fetch(value) { false } }
13
- # TODO: Improve parsing!
14
- # In case date parsing fails, ArgumentError is thrown. Currently, it is rescued in Repository::Loaders.find.
15
- # That method assumes that the exception is due to invalid argument to the find method, which is not the case
16
- # if it is raised from here!
17
- Date = Types::Date.optional.constructor { |value| ::Date.parse(value.to_s) unless value.nil? || value == '' }
9
+ String = Types::Strict::String.optional.constructor { |value| value&.to_s }
10
+ Float = Types::Strict::Float.optional.constructor { |value| value&.to_f }
11
+ Integer = Types::Strict::Integer.optional.constructor { |value| value&.to_i }
12
+ Boolean = Types::Bool.optional.constructor { |value| THE_TRUTH.fetch(value, false) }
13
+ Date = Types::Date.optional.constructor do |value|
14
+ next if value.nil? || value == ''
15
+
16
+ begin
17
+ ::Date.parse(value.to_s)
18
+ rescue ::Date::Error
19
+ raise Fortnox::API::AttributeError, 'invalid date'
20
+ end
21
+ end
18
22
  end
19
23
  end
20
24
  end
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'fortnox/api/types/document_row'
3
+ require_relative 'document_row'
4
4
 
5
5
  module Fortnox
6
6
  module API
@@ -4,9 +4,9 @@ module Fortnox
4
4
  module API
5
5
  module Types
6
6
  module Required
7
- String = Types::Strict::String.constructor { |value| value.to_s unless value.nil? }.is(:required)
8
- Integer = Types::Strict::Int.constructor { |value| value.to_i unless value.nil? }.is(:required)
9
- Float = Types::Strict::Float.constructor { |value| value.to_f unless value.nil? }.is(:required)
7
+ String = Types::Strict::String.constructor { |value| value&.to_s }.is(:required)
8
+ Integer = Types::Strict::Integer.constructor { |value| value&.to_i }.is(:required)
9
+ Float = Types::Strict::Float.constructor { |value| value&.to_f }.is(:required)
10
10
  end
11
11
  end
12
12
  end
@@ -7,15 +7,15 @@ module Fortnox
7
7
  module String
8
8
  def self.[](size)
9
9
  Types::Strict::String.constrained(max_size: size).optional.constructor do |value|
10
- value.to_s unless value.nil?
10
+ value&.to_s
11
11
  end
12
12
  end
13
13
  end
14
14
 
15
15
  module Integer
16
16
  def self.[](low, high)
17
- Types::Strict::Int.constrained(gteq: low, lteq: high).optional.constructor do |value|
18
- value.to_i unless value.nil?
17
+ Types::Strict::Integer.constrained(gteq: low, lteq: high).optional.constructor do |value|
18
+ value&.to_i
19
19
  end
20
20
  end
21
21
  end
@@ -23,7 +23,7 @@ module Fortnox
23
23
  module Float
24
24
  def self.[](low, high)
25
25
  Types::Strict::Float.constrained(gteq: low, lteq: high).optional.constructor do |value|
26
- value.to_f unless value.nil?
26
+ value&.to_f
27
27
  end
28
28
  end
29
29
  end
@@ -3,7 +3,7 @@
3
3
  require 'dry-struct'
4
4
  require 'dry-types'
5
5
  require 'countries'
6
- require 'fortnox/api/types/shim/country_string'
6
+ require_relative 'types/shim/country_string'
7
7
 
8
8
  module Dry
9
9
  module Types
@@ -12,11 +12,7 @@ module Dry
12
12
  new_options = option_names.each_with_object({}) do |name, hash|
13
13
  hash[name] = true
14
14
  end
15
- with(new_options)
16
- end
17
-
18
- def is?(option_name)
19
- @options[option_name]
15
+ with(**new_options)
20
16
  end
21
17
  end
22
18
  end
@@ -25,24 +21,24 @@ end
25
21
  module Fortnox
26
22
  module API
27
23
  module Types
28
- include Dry::Types.module
24
+ include Dry.Types(default: :nominal)
29
25
  ISO3166.configure { |config| config.locales = %i[en sv] }
30
26
 
31
27
  THE_TRUTH = { true => true, 'true' => true, false => false, 'false' => false }.freeze
32
28
 
33
- require 'fortnox/api/types/required'
34
- require 'fortnox/api/types/defaulted'
35
- require 'fortnox/api/types/nullable'
36
-
37
- require 'fortnox/api/types/enums'
29
+ require_relative 'types/required'
30
+ require_relative 'types/defaulted'
31
+ require_relative 'types/nullable'
38
32
 
39
- require 'fortnox/api/types/sized'
33
+ require_relative 'types/enums'
34
+ require_relative 'types/sized'
40
35
 
41
- AccountNumber = Strict::Int
36
+ AccountNumber = Strict::Integer
42
37
  .constrained(gteq: 0, lteq: 9999)
43
38
  .optional
44
39
  .constructor do |value|
45
40
  next nil if value.nil? || value == ''
41
+
46
42
  value
47
43
  end
48
44
 
@@ -60,7 +56,7 @@ module Fortnox
60
56
  next CountryString.new('SE') if value.match?(/^s(e$|we|ve)/i)
61
57
 
62
58
  country = ::ISO3166::Country[value] ||
63
- ::ISO3166::Country.find_country_by_name(value) ||
59
+ ::ISO3166::Country.find_country_by_any_name(value) ||
64
60
  ::ISO3166::Country.find_country_by_translated_names(value)
65
61
 
66
62
  raise Dry::Types::ConstraintError.new('value violates constraints', value) if country.nil?
@@ -97,7 +93,7 @@ module Fortnox
97
93
  Email = Strict::String
98
94
  .constrained(max_size: 1024, format: /^$|\A[[[:alnum:]]+-_.]+@[\w+-_.]+\.[a-z]+\z/i)
99
95
  .optional
100
- .constructor { |v| v.to_s.downcase unless v.nil? }
96
+ .constructor { |v| v&.to_s&.downcase }
101
97
 
102
98
  HouseworkType = Strict::String
103
99
  .constrained(included_in: HouseworkTypes.values)
@@ -124,13 +120,25 @@ module Fortnox
124
120
  .optional
125
121
  .constructor(EnumConstructors.lower_case)
126
122
 
127
- require 'fortnox/api/types/model'
128
- require 'fortnox/api/types/default_delivery_types'
129
- require 'fortnox/api/types/default_templates'
130
- require 'fortnox/api/types/email_information'
131
- require 'fortnox/api/types/edi_information'
132
- require 'fortnox/api/types/invoice_row'
133
- require 'fortnox/api/types/order_row'
123
+ # Some Fortnox endpoints returns a String and some returns an Integer...
124
+ # The documentation says it should be a string, so let's keep it as a string.
125
+ SalesAccount = Strict::String
126
+ .constrained(format: /^[0-9]{4}$/)
127
+ .optional
128
+ .constructor do |value|
129
+ next nil if value == '' || value.nil?
130
+ next value.to_s if value.is_a?(::Integer)
131
+
132
+ value
133
+ end
134
+
135
+ require_relative 'types/model'
136
+ require_relative 'types/default_delivery_types'
137
+ require_relative 'types/default_templates'
138
+ require_relative 'types/email_information'
139
+ require_relative 'types/edi_information'
140
+ require_relative 'types/invoice_row'
141
+ require_relative 'types/order_row'
134
142
  end
135
143
  end
136
144
  end
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Fortnox
4
4
  module API
5
- VERSION = '0.8.0'
5
+ VERSION = '0.9.0'
6
6
  end
7
7
  end
data/lib/fortnox/api.rb CHANGED
@@ -1,46 +1,39 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'set'
4
- require 'dry-struct'
5
- require 'fortnox/api/circular_queue'
6
- require 'fortnox/api/version'
4
+ require 'dry-configurable'
5
+ require 'dry-container'
7
6
  require 'logger'
8
7
 
8
+ require_relative 'api/version'
9
+
9
10
  module Fortnox
10
11
  module API
11
12
  extend Dry::Configurable
12
13
 
13
14
  DEFAULT_CONFIGURATION = {
14
15
  base_url: 'https://api.fortnox.se/3/',
15
- client_secret: nil,
16
- token_store: {},
17
- access_token: nil,
18
- access_tokens: nil,
16
+ token_url: 'https://apps.fortnox.se/oauth-v1/token',
19
17
  debugging: false,
20
18
  logger: lambda {
21
- logger = Logger.new(STDOUT)
19
+ logger = Logger.new($stdout)
22
20
  logger.level = Logger::WARN
23
21
  return logger
24
22
  }.call
25
23
  }.freeze
26
24
 
27
- setting :base_url, DEFAULT_CONFIGURATION[:base_url]
28
- setting :client_secret, DEFAULT_CONFIGURATION[:client_secret]
29
- setting :token_store, DEFAULT_CONFIGURATION[:token_store]
30
- setting :access_token, DEFAULT_CONFIGURATION[:access_token] do |value|
31
- next if value.nil? # nil is a valid unassigned value
32
- invalid_access_token_format!(value) unless value.is_a?(String)
33
- config.token_store = { default: value }
34
- value
25
+ setting :base_url, default: DEFAULT_CONFIGURATION[:base_url]
26
+ setting :token_url, default: DEFAULT_CONFIGURATION[:token_url]
27
+ setting :debugging, default: DEFAULT_CONFIGURATION[:debugging], reader: true
28
+ setting :logger, default: DEFAULT_CONFIGURATION[:logger], reader: true
29
+
30
+ def self.access_token=(token)
31
+ Thread.current[:access_token] = token
35
32
  end
36
- setting :access_tokens, DEFAULT_CONFIGURATION[:access_tokens] do |value|
37
- next if value.nil? # nil is a valid unassigned value
38
- invalid_access_tokens_format!(value) unless value.is_a?(Hash) || value.is_a?(Array)
39
- config.token_store = value.is_a?(Hash) ? value : { default: value }
40
- value
33
+
34
+ def self.access_token
35
+ Thread.current[:access_token]
41
36
  end
42
- setting :debugging, DEFAULT_CONFIGURATION[:debugging], reader: true
43
- setting :logger, DEFAULT_CONFIGURATION[:logger], reader: true
44
37
 
45
38
  class Exception < StandardError
46
39
  end
@@ -57,24 +50,13 @@ module Fortnox
57
50
  class MissingConfiguration < Fortnox::API::Exception
58
51
  end
59
52
 
60
- Registry = Dry::Container.new
61
-
62
- def self.invalid_access_token_format!(value)
63
- raise ArgumentError,
64
- 'expected a String, but '\
65
- "#{value.inspect} is a(n) #{value.class}"
53
+ class MissingAccessToken < Fortnox::API::Exception
66
54
  end
67
- private_class_method :invalid_access_token_format!
68
55
 
69
- def self.invalid_access_tokens_format!(value)
70
- raise ArgumentError,
71
- 'expected a Hash or an Array, but '\
72
- "#{value.inspect} is a(n) #{value.class}"
73
- end
74
- private_class_method :invalid_access_tokens_format!
56
+ Registry = Dry::Container.new
75
57
  end
76
58
  end
77
59
 
78
- require 'fortnox/api/models'
79
- require 'fortnox/api/repositories'
80
- require 'fortnox/api/mappers'
60
+ require_relative 'api/models'
61
+ require_relative 'api/repositories'
62
+ require_relative 'api/mappers'
@@ -6,27 +6,29 @@ require 'fortnox/api/mappers/base/canonical_name_sym'
6
6
  describe Fortnox::API::Mapper::CanonicalNameSym do
7
7
  describe '.canonical_name_sym' do
8
8
  context 'with simple class' do
9
- using_test_class do
10
- class TestClass
9
+ subject { TestClass.canonical_name_sym }
10
+
11
+ before do
12
+ test_class = Class.new do
11
13
  extend Fortnox::API::Mapper::CanonicalNameSym
12
14
  end
13
- end
14
15
 
15
- subject { TestClass.canonical_name_sym }
16
+ stub_const('TestClass', test_class)
17
+ end
16
18
 
17
19
  it { is_expected.to eq(:testclass) }
18
20
  end
19
21
 
20
22
  context 'when class included in module' do
21
- using_test_class do
22
- module Something
23
- class Test
24
- extend Fortnox::API::Mapper::CanonicalNameSym
25
- end
23
+ subject { Something::Test.canonical_name_sym }
24
+
25
+ before do
26
+ test_class = Class.new do
27
+ extend Fortnox::API::Mapper::CanonicalNameSym
26
28
  end
27
- end
28
29
 
29
- subject { Something::Test.canonical_name_sym }
30
+ stub_const('Something::Test', test_class)
31
+ end
30
32
 
31
33
  it { is_expected.to eq(:test) }
32
34
  end
@@ -8,14 +8,12 @@ describe Fortnox::API::Mapper::FromJSON do
8
8
  include_context 'with JSON conversion'
9
9
 
10
10
  before do
11
- module Test
12
- class BaseMapper
13
- include Fortnox::API::Mapper::FromJSON
14
- end
11
+ Test::BaseMapper.class_eval do
12
+ include Fortnox::API::Mapper::FromJSON # rubocop:disable RSpec/DescribedClass
15
13
  end
16
14
 
17
- register_mapper(:categories, Test::CategoryMapper)
18
- register_mapper(:designer, Test::ProductDesignerMapper)
15
+ add_to_registry(:categories, Test::CategoryMapper)
16
+ add_to_registry(:designer, Test::ProductDesignerMapper)
19
17
  end
20
18
 
21
19
  let(:mapper) { Test::ProductMapper.new }
@@ -26,10 +24,10 @@ describe Fortnox::API::Mapper::FromJSON do
26
24
  {
27
25
  'Product' => {
28
26
  '@url': 'someurl@example.com',
29
- 'Name': 'Ford Mustang',
30
- 'VAT': 30_000,
31
- 'Categories': [{ 'Name' => 'Cars', 'ID' => '1' }, { 'Name' => 'Fast cars', 'ID' => '2' }],
32
- 'Designer': { 'Name' => 'John Najjar', 'ID' => '23' }
27
+ Name: 'Ford Mustang',
28
+ VAT: 30_000,
29
+ Categories: [{ Name: 'Cars', ID: '1' }, { Name: 'Fast cars', ID: '2' }],
30
+ Designer: { Name: 'John Najjar', ID: '23' }
33
31
  }
34
32
  }
35
33
  end
@@ -47,7 +45,7 @@ describe Fortnox::API::Mapper::FromJSON do
47
45
  # expect( entity_hash[:url] ).to eq 'someurl@example.com'
48
46
  # end
49
47
 
50
- context 'with nested models' do
48
+ describe 'nested models' do
51
49
  let(:expected_nested_model_hash) do
52
50
  [{ name: 'Cars', id: '1' }, { name: 'Fast cars', id: '2' }]
53
51
  end
@@ -57,7 +55,7 @@ describe Fortnox::API::Mapper::FromJSON do
57
55
  end
58
56
  end
59
57
 
60
- context 'with nested model' do
58
+ describe 'nested model' do
61
59
  let(:expected_nested_model_hash) { { name: 'John Najjar', id: '23' } }
62
60
 
63
61
  specify 'is converted correctly' do