fortnox-api 0.7.2 → 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 -19
  9. data/CHANGELOG.md +66 -1
  10. data/CONTRIBUTE.md +21 -9
  11. data/DEVELOPER_README.md +72 -0
  12. data/Guardfile +13 -4
  13. data/README.md +226 -61
  14. data/Rakefile +133 -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 +7 -6
  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 +5 -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 +54 -10
  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 +37 -24
  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 +72 -29
  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/enums_spec.rb +1 -0
  103. data/spec/fortnox/api/types/examples/document_row.rb +3 -3
  104. data/spec/fortnox/api/types/examples/enum.rb +4 -4
  105. data/spec/fortnox/api/types/examples/types.rb +1 -3
  106. data/spec/fortnox/api/types/housework_types_spec.rb +124 -43
  107. data/spec/fortnox/api/types/model_spec.rb +13 -23
  108. data/spec/fortnox/api/types/nullable_spec.rb +30 -10
  109. data/spec/fortnox/api/types/order_row_spec.rb +2 -2
  110. data/spec/fortnox/api/types/required_spec.rb +7 -15
  111. data/spec/fortnox/api/types/sales_account_spec.rb +57 -0
  112. data/spec/fortnox/api_spec.rb +19 -124
  113. data/spec/spec_helper.rb +0 -14
  114. data/spec/support/helpers/configuration_helper.rb +30 -3
  115. data/spec/support/helpers.rb +1 -1
  116. data/spec/support/matchers/type/attribute_matcher.rb +2 -2
  117. data/spec/support/matchers/type/enum_matcher.rb +1 -1
  118. data/spec/support/matchers/type/have_account_number_matcher.rb +1 -1
  119. data/spec/support/matchers/type/have_email_matcher.rb +1 -1
  120. data/spec/support/matchers/type/have_nullable_date_matcher.rb +7 -5
  121. data/spec/support/matchers/type/have_nullable_matcher.rb +1 -1
  122. data/spec/support/matchers/type/have_nullable_string_matcher.rb +5 -5
  123. data/spec/support/matchers/type/require_attribute_matcher.rb +5 -5
  124. data/spec/support/matchers/type/type_matcher.rb +1 -1
  125. data/spec/support/vcr_setup.rb +16 -0
  126. data/spec/vcr_cassettes/articles/all.yml +41 -42
  127. data/spec/vcr_cassettes/articles/find_by_hash_failure.yml +36 -19
  128. data/spec/vcr_cassettes/articles/find_failure.yml +36 -19
  129. data/spec/vcr_cassettes/articles/find_id_1.yml +38 -20
  130. data/spec/vcr_cassettes/articles/find_new.yml +39 -22
  131. data/spec/vcr_cassettes/articles/multi_param_find_by_hash.yml +38 -21
  132. data/spec/vcr_cassettes/articles/save_new.yml +37 -19
  133. data/spec/vcr_cassettes/articles/save_old.yml +39 -22
  134. data/spec/vcr_cassettes/articles/save_with_specially_named_attribute.yml +37 -19
  135. data/spec/vcr_cassettes/articles/search_by_name.yml +41 -21
  136. data/spec/vcr_cassettes/articles/search_miss.yml +36 -19
  137. data/spec/vcr_cassettes/articles/search_with_special_char.yml +36 -19
  138. data/spec/vcr_cassettes/articles/single_param_find_by_hash.yml +38 -32
  139. data/spec/vcr_cassettes/authentication/expired_token.yml +54 -0
  140. data/spec/vcr_cassettes/authentication/invalid_authorization.yml +57 -0
  141. data/spec/vcr_cassettes/authentication/invalid_refresh_token.yml +58 -0
  142. data/spec/vcr_cassettes/authentication/valid_request.yml +63 -0
  143. data/spec/vcr_cassettes/customers/all.yml +44 -132
  144. data/spec/vcr_cassettes/customers/find_by_hash_failure.yml +36 -19
  145. data/spec/vcr_cassettes/customers/find_failure.yml +36 -19
  146. data/spec/vcr_cassettes/customers/find_id_1.yml +39 -21
  147. data/spec/vcr_cassettes/customers/find_new.yml +38 -21
  148. data/spec/vcr_cassettes/customers/find_with_sales_account.yml +63 -0
  149. data/spec/vcr_cassettes/customers/multi_param_find_by_hash.yml +38 -21
  150. data/spec/vcr_cassettes/customers/save_new.yml +36 -18
  151. data/spec/vcr_cassettes/customers/save_new_with_country_code_SE.yml +32 -24
  152. data/spec/vcr_cassettes/customers/save_new_with_sales_account.yml +63 -0
  153. data/spec/vcr_cassettes/customers/save_old.yml +38 -21
  154. data/spec/vcr_cassettes/customers/save_with_specially_named_attribute.yml +36 -18
  155. data/spec/vcr_cassettes/customers/search_by_name.yml +38 -48
  156. data/spec/vcr_cassettes/customers/search_miss.yml +36 -19
  157. data/spec/vcr_cassettes/customers/search_with_special_char.yml +36 -19
  158. data/spec/vcr_cassettes/customers/single_param_find_by_hash.yml +39 -22
  159. data/spec/vcr_cassettes/invoices/all.yml +71 -114
  160. data/spec/vcr_cassettes/invoices/filter_hit.yml +39 -24
  161. data/spec/vcr_cassettes/invoices/filter_invalid.yml +35 -17
  162. data/spec/vcr_cassettes/invoices/find_by_hash_failure.yml +36 -19
  163. data/spec/vcr_cassettes/invoices/find_failure.yml +36 -19
  164. data/spec/vcr_cassettes/invoices/find_id_1.yml +40 -22
  165. data/spec/vcr_cassettes/invoices/find_new.yml +41 -24
  166. data/spec/vcr_cassettes/invoices/multi_param_find_by_hash.yml +38 -21
  167. data/spec/vcr_cassettes/invoices/row_description_limit.yml +65 -0
  168. data/spec/vcr_cassettes/invoices/save_new.yml +39 -21
  169. data/spec/vcr_cassettes/invoices/save_new_with_comments.yml +39 -21
  170. data/spec/vcr_cassettes/invoices/save_new_with_country.yml +35 -26
  171. data/spec/vcr_cassettes/invoices/save_new_with_country_GB.yml +36 -27
  172. data/spec/vcr_cassettes/invoices/save_new_with_country_Norge.yml +35 -26
  173. data/spec/vcr_cassettes/invoices/save_new_with_country_Norway.yml +35 -26
  174. data/spec/vcr_cassettes/invoices/save_new_with_country_Sverige.yml +35 -26
  175. data/spec/vcr_cassettes/invoices/save_new_with_country_VA.yml +36 -27
  176. data/spec/vcr_cassettes/invoices/save_new_with_country_VI.yml +36 -27
  177. data/spec/vcr_cassettes/invoices/save_new_with_country_empty_string.yml +35 -26
  178. data/spec/vcr_cassettes/invoices/save_new_with_country_nil.yml +35 -26
  179. data/spec/vcr_cassettes/invoices/save_new_with_unsaved_parent.yml +65 -0
  180. data/spec/vcr_cassettes/invoices/save_old.yml +41 -24
  181. data/spec/vcr_cassettes/invoices/save_old_with_empty_comments.yml +41 -24
  182. data/spec/vcr_cassettes/invoices/save_old_with_empty_country.yml +37 -29
  183. data/spec/vcr_cassettes/invoices/save_old_with_nil_comments.yml +41 -24
  184. data/spec/vcr_cassettes/invoices/save_old_with_nil_country.yml +37 -29
  185. data/spec/vcr_cassettes/invoices/save_with_nested_model.yml +40 -21
  186. data/spec/vcr_cassettes/invoices/save_with_specially_named_attribute.yml +39 -20
  187. data/spec/vcr_cassettes/invoices/search_by_name.yml +38 -27
  188. data/spec/vcr_cassettes/invoices/search_miss.yml +36 -19
  189. data/spec/vcr_cassettes/invoices/search_with_special_char.yml +36 -19
  190. data/spec/vcr_cassettes/invoices/single_param_find_by_hash.yml +39 -22
  191. data/spec/vcr_cassettes/orders/all.yml +44 -119
  192. data/spec/vcr_cassettes/orders/filter_hit.yml +39 -26
  193. data/spec/vcr_cassettes/orders/filter_invalid.yml +35 -17
  194. data/spec/vcr_cassettes/orders/find_by_hash_failure.yml +36 -19
  195. data/spec/vcr_cassettes/orders/find_failure.yml +36 -19
  196. data/spec/vcr_cassettes/orders/find_id_1.yml +42 -23
  197. data/spec/vcr_cassettes/orders/find_new.yml +41 -24
  198. data/spec/vcr_cassettes/orders/housework_invalid_tax_reduction_type.yml +61 -0
  199. data/spec/vcr_cassettes/orders/housework_othercoses_invalid.yml +61 -0
  200. data/spec/vcr_cassettes/orders/housework_type_babysitting.yml +40 -21
  201. data/spec/vcr_cassettes/orders/housework_type_cleaning.yml +40 -21
  202. data/spec/vcr_cassettes/orders/housework_type_construction.yml +40 -21
  203. data/spec/vcr_cassettes/orders/housework_type_cooking.yml +36 -18
  204. data/spec/vcr_cassettes/orders/housework_type_electricity.yml +40 -21
  205. data/spec/vcr_cassettes/orders/housework_type_gardening.yml +40 -21
  206. data/spec/vcr_cassettes/orders/housework_type_glassmetalwork.yml +40 -21
  207. data/spec/vcr_cassettes/orders/housework_type_grounddrainagework.yml +40 -21
  208. data/spec/vcr_cassettes/orders/housework_type_hvac.yml +40 -21
  209. data/spec/vcr_cassettes/orders/housework_type_itservices.yml +65 -0
  210. data/spec/vcr_cassettes/orders/housework_type_majorappliancerepair.yml +65 -0
  211. data/spec/vcr_cassettes/orders/housework_type_masonry.yml +40 -21
  212. data/spec/vcr_cassettes/orders/housework_type_movingservices.yml +65 -0
  213. data/spec/vcr_cassettes/orders/housework_type_othercare.yml +40 -21
  214. data/spec/vcr_cassettes/orders/housework_type_othercosts.yml +40 -21
  215. data/spec/vcr_cassettes/orders/housework_type_paintingwallpapering.yml +40 -21
  216. data/spec/vcr_cassettes/orders/housework_type_snowplowing.yml +40 -21
  217. data/spec/vcr_cassettes/orders/housework_type_textileclothing.yml +40 -21
  218. data/spec/vcr_cassettes/orders/housework_type_tutoring.yml +36 -18
  219. data/spec/vcr_cassettes/orders/multi_param_find_by_hash.yml +38 -21
  220. data/spec/vcr_cassettes/orders/save_new.yml +40 -22
  221. data/spec/vcr_cassettes/orders/save_old.yml +41 -24
  222. data/spec/vcr_cassettes/orders/save_with_nested_model.yml +40 -21
  223. data/spec/vcr_cassettes/orders/search_by_name.yml +38 -23
  224. data/spec/vcr_cassettes/orders/search_miss.yml +36 -19
  225. data/spec/vcr_cassettes/orders/search_with_special_char.yml +36 -19
  226. data/spec/vcr_cassettes/orders/single_param_find_by_hash.yml +39 -22
  227. data/spec/vcr_cassettes/projects/all.yml +39 -37
  228. data/spec/vcr_cassettes/projects/find_by_hash_failure.yml +36 -19
  229. data/spec/vcr_cassettes/projects/find_failure.yml +36 -19
  230. data/spec/vcr_cassettes/projects/find_id_1.yml +38 -21
  231. data/spec/vcr_cassettes/projects/find_new.yml +39 -22
  232. data/spec/vcr_cassettes/projects/multi_param_find_by_hash.yml +40 -22
  233. data/spec/vcr_cassettes/projects/save_new.yml +37 -19
  234. data/spec/vcr_cassettes/projects/save_old.yml +39 -22
  235. data/spec/vcr_cassettes/projects/single_param_find_by_hash.yml +38 -21
  236. data/spec/vcr_cassettes/termsofpayments/all.yml +43 -29
  237. data/spec/vcr_cassettes/termsofpayments/find_failure.yml +36 -19
  238. data/spec/vcr_cassettes/termsofpayments/find_id_1.yml +38 -22
  239. data/spec/vcr_cassettes/termsofpayments/find_new.yml +38 -21
  240. data/spec/vcr_cassettes/termsofpayments/save_new.yml +37 -19
  241. data/spec/vcr_cassettes/termsofpayments/save_old.yml +38 -21
  242. data/spec/vcr_cassettes/units/all.yml +38 -26
  243. data/spec/vcr_cassettes/units/find_failure.yml +36 -19
  244. data/spec/vcr_cassettes/units/find_id_1.yml +38 -21
  245. data/spec/vcr_cassettes/units/find_new.yml +38 -21
  246. data/spec/vcr_cassettes/units/save_new.yml +37 -19
  247. data/spec/vcr_cassettes/units/save_old.yml +38 -21
  248. data/spec/vcr_cassettes/units/save_with_specially_named_attribute.yml +37 -19
  249. metadata +133 -252
  250. data/lib/fortnox/api/circular_queue.rb +0 -39
  251. data/spec/fortnox/api/circular_queue_spec.rb +0 -52
  252. data/spec/support/helpers/dummy_class_helper.rb +0 -38
  253. data/spec/support/helpers/when_performing_helper.rb +0 -7
  254. data/spec/vcr_cassettes/invoices/save_new_with_country_KR.yml +0 -57
  255. data/temp.txt +0 -1
data/README.md CHANGED
@@ -1,87 +1,154 @@
1
1
  # Fortnox API
2
- > Wrapper gem for Fortnox AB's version 3 REST(ish) API. If you need to integrate an existing or new Ruby or Rails app against Fortnox this gem will save you a lot of time, you are welcome. Feel free to repay the community with some nice PRs of your own :simple_smile:
2
+
3
+ Wrapper gem for Fortnox AB's version 3 REST(ish) API. If you need to integrate
4
+ an existing or new Ruby or Rails app against Fortnox this gem will save you a
5
+ lot of time, you are welcome. Feel free to repay the community with some nice
6
+ PRs of your own 😃
3
7
 
4
8
  # Status for master
9
+
5
10
  [![Gem version](https://img.shields.io/gem/v/fortnox-api.svg?style=flat-square)](https://rubygems.org/gems/fortnox-api)
6
- [![Build Status](https://travis-ci.com/accodeing/fortnox-api.svg?branch=master)](https://travis-ci.com/accodeing/fortnox-api)
11
+ [![Build Status](https://app.travis-ci.com/ehannes/fortnox-api.svg?branch=master)](https://app.travis-ci.com/github/accodeing/fortnox-api)
7
12
 
8
13
  # Status for development
9
- [![Build Status](https://travis-ci.com/accodeing/fortnox-api.svg?branch=development)](https://travis-ci.com/accodeing/fortnox-api)
14
+
15
+ [![Build Status](https://app.travis-ci.com/ehannes/fortnox-api.svg?branch=development)](https://app.travis-ci.com/github/accodeing/fortnox-api)
10
16
  [![Maintainability](https://api.codeclimate.com/v1/badges/89d30a43fedf210d470b/maintainability)](https://codeclimate.com/github/accodeing/fortnox-api/maintainability)
11
17
  [![Test Coverage](https://api.codeclimate.com/v1/badges/89d30a43fedf210d470b/test_coverage)](https://codeclimate.com/github/accodeing/fortnox-api/test_coverage)
12
18
 
13
- The rough status of this project is as follows (as of October 2018):
14
- * Development is not as active as it used to be, but the project is not forgotten. We have an app running this gem in production and it works like a charm for what we do.
15
- * We are planning on generalize REST API's in general with our [rest_easy gem](https://github.com/accodeing/rest_easy).
16
- * Basic structure complete. Things like getting customers and invoices, updating and saving etc.
17
- * Some advanced features implemented, for instance support for multiple Access Tokens and filtering entities.
18
- * We have ideas for more advanced features, like sorting entities, pagination of results but nothing in the pipeline right now.
19
- * A few models implemented. Right now we pretty good support for `Customer`, `Invoice`, `Order`, `Article`, `Label` and `Project`. Adding more models in general is quick and easy (that's the whole point with this gem), see the developer guide further down.
20
- * Massive refactorings no longer occurs weekly :)
19
+ The rough status of this project is as follows (as of spring 2023):
20
+
21
+ - `master` branch and the released versions should be production ready.
22
+ - We are actively working on our generalization of this gem:
23
+ [rest_easy gem](https://github.com/accodeing/rest_easy). It will be a base for
24
+ REST API's in general.
25
+ - Basic structure complete. Things like getting customers and invoices, updating
26
+ and saving etc.
27
+ - Some advanced features implemented, for instance support for multiple Fortnox
28
+ accounts and filtering entities.
29
+ - We have ideas for more advanced features, like sorting entities, pagination of
30
+ results but it's not implemented...
31
+ - A few models implemented. Right now we pretty good support for `Customer`,
32
+ `Invoice`, `Order`, `Article`, `Label` and `Project`. Adding more models in
33
+ general is quick and easy (that's the whole point with this gem), see the
34
+ developer guide further down.
21
35
 
22
36
  # Architecture overview
23
- The gem is structured with distinct models for the tasks of data, JSON mapping and saving state. These are called: model, type, mapper and repository.
24
37
 
25
- If you come from a Rails background and have not been exposed to other ways of structuring the solution to the CRUD problem this might seem strange to you since ActiveRecord merges these roles into the `ActiveRecord::Base` class.
38
+ The gem is structured with distinct models for the tasks of data, JSON mapping
39
+ and saving state. These are called: model, type, mapper and repository.
26
40
 
27
- To keep it simple: The active record pattern (as implemented by Rails) is easier to work with if you only have one data source, the database, in your application. The data mapper pattern is easier to work with if you have several data sources, such as different databases, external APIs and flat files on disk etc, in your application. It's also easier to compose the data mapper components into active record like classes than to separate active records parts to get a data mapper style structure.
41
+ If you come from a Rails background and have not been exposed to other ways of
42
+ structuring the solution to the CRUD problem this might seem strange to you
43
+ since ActiveRecord merges these roles into the `ActiveRecord::Base` class.
28
44
 
29
- If you are interested in a more detailed description of the difference between the two architectures you can read this post that explains it well using simple examples: [What’s the difference between Active Record and Data Mapper?](http://culttt.com/2014/06/18/whats-difference-active-record-data-mapper/)
45
+ To keep it simple: The active record pattern (as implemented by Rails) is easier
46
+ to work with if you only have one data source, the database, in your
47
+ application. The data mapper pattern is easier to work with if you have several
48
+ data sources, such as different databases, external APIs and flat files on disk
49
+ etc, in your application. It's also easier to compose the data mapper components
50
+ into active record like classes than to separate active records parts to get a
51
+ data mapper style structure.
52
+
53
+ If you are interested in a more detailed description of the difference between
54
+ the two architectures you can read this post that explains it well using simple
55
+ examples:
56
+ [What’s the difference between Active Record and Data Mapper?](http://culttt.com/2014/06/18/whats-difference-active-record-data-mapper/)
30
57
 
31
58
  ## Model
32
- The model role classes serve as dumb data objects. They do have some logic to coheres values etc, but they do not contain validation logic nor any business logic at all.
59
+
60
+ The model role classes serve as dumb data objects. They do have some logic to
61
+ coheres values etc, but they do not contain validation logic nor any business
62
+ logic at all.
33
63
 
34
64
  ### Attribute
35
- Several of the models share attributes. One example is account, as in a `Bookkeeping` account number. These attributes have the same definition, cohesion and validation logic so it makes sense to extract them from the models and put them in separate classes. For more information, see Types below.
65
+
66
+ Several of the models share attributes. One example is account, as in a
67
+ `Bookkeeping` account number. These attributes have the same definition,
68
+ cohesion and validation logic so it makes sense to extract them from the models
69
+ and put them in separate classes. For more information, see Types below.
36
70
 
37
71
  ### Immutability
72
+
38
73
  The model instances are immutable. That means:
74
+
39
75
  ```ruby
40
76
  customer.name # => "Old Name"
41
77
  customer.name = 'New Name' # => "New Name"
42
78
 
43
79
  customer.name == "New Name" # => false
44
80
  ```
45
- Normally you would expect an assignment to mutate the instance and update the `name` field. Immutability explicitly means that you can't mutate state this way, any operation that attempts to update state needs to return a new instance with the updated state while leaving the old instance alone.
81
+
82
+ Normally you would expect an assignment to mutate the instance and update the
83
+ `name` field. Immutability explicitly means that you can't mutate state this
84
+ way, any operation that attempts to update state needs to return a new instance
85
+ with the updated state while leaving the old instance alone.
46
86
 
47
87
  So you might think you should do this instead:
88
+
48
89
  ```ruby
49
90
  customer = customer.name = 'New Name' # => "New Name"
50
91
  ```
51
- But if you are familiar with chaining assignments in Ruby you will see that this does not work. The result of any assignment, `LHS = RHS`, operation in Ruby is `RHS`. Even if you implement your own `=` method and explicitly return something else. This is a feature of the language and not something we can get around. So instead you have to do:
92
+
93
+ But if you are familiar with chaining assignments in Ruby you will see that this
94
+ does not work. The result of any assignment, `LHS = RHS`, operation in Ruby is
95
+ `RHS`. Even if you implement your own `=` method and explicitly return something
96
+ else. This is a feature of the language and not something we can get around. So
97
+ instead you have to do:
98
+
52
99
  ```ruby
53
100
  customer.name # => "Old Name"
54
101
  updated_customer = customer.update( name: 'New Name' ) # => <Fortnox::API::Model::Customer:0x007fdf22949298 ... >
55
102
  updated_customer.name == "New Name" # => true
56
103
  ```
104
+
57
105
  And note that:
106
+
58
107
  ```ruby
59
108
  customer.name # => "Old Name"
60
109
  customer.update( name: 'New Name' ) # => <Fortnox::API::Model::Customer:0x007fdf21100b00 ... >
61
110
  customer.name == "New Name" # => false
62
111
  ```
112
+
63
113
  This is how all the models work, they are all immutable.
64
114
 
65
115
  ### Exceptions
66
116
 
67
- Models can throw `Fortnox::API::AttributeError` if an attribute is invalid in some way (for instance if you try to assign a too long string to a limited string attribute) and `Fortnox::API::MissingAttributeError` if a required attribute is missing.
117
+ Models can throw `Fortnox::API::AttributeError` if an attribute is invalid in
118
+ some way (for instance if you try to assign a too long string to a limited
119
+ string attribute) and `Fortnox::API::MissingAttributeError` if a required
120
+ attribute is missing.
68
121
 
69
122
  ## Type
70
- The types automatically enforce the constraints on values, lengths and, in some cases, content of the model attributes. Types forces your models to be correct before sending data to the API, which saves you a lot of API calls and rescuing the exception we throw when we get a 4xx/5xx response from the server (you can still get errors from the server; our implementation is not perfect. Also, Fortnox sometimes requires a specific combination of attributes).
123
+
124
+ The types automatically enforce the constraints on values, lengths and, in some
125
+ cases, content of the model attributes. Types forces your models to be correct
126
+ before sending data to the API, which saves you a lot of API calls and rescuing
127
+ the exception we throw when we get a 4xx/5xx response from the server (you can
128
+ still get errors from the server; our implementation is not perfect. Also,
129
+ Fortnox sometimes requires a specific combination of attributes).
71
130
 
72
131
  ## Repositories
73
- Used to load, update, create and delete model instances. These are what is actually wrapping the HTTP REST API requests against Fortnox's server.
132
+
133
+ Used to load, update, create and delete model instances. These are what is
134
+ actually wrapping the HTTP REST API requests against Fortnox's server.
74
135
 
75
136
  ### Exceptions
76
137
 
77
- Repositories can throw `Fortnox::API::RemoteServerError` if something went wrong at Fortnox.
138
+ Repositories can throw `Fortnox::API::RemoteServerError` if something went wrong
139
+ at Fortnox.
78
140
 
79
141
  ## Mappers
80
- These are responsible for the mapping between our plain old Ruby object models and Fortnox JSON requests. The repositories use the mappers to map models to JSON requests and JSON to model instances when working with the Fortnox API, you will not need to use them directly.
142
+
143
+ These are responsible for the mapping between our plain old Ruby object models
144
+ and Fortnox JSON requests. The repositories use the mappers to map models to
145
+ JSON requests and JSON to model instances when working with the Fortnox API, you
146
+ will not need to use them directly.
81
147
 
82
148
  # Requirements
83
149
 
84
- This gem is build for Ruby 2.4 or higher (see Travis configuration file for what versions we are testing against).
150
+ This gem is built for Ruby 2.6 or higher (see Travis configuration file for what
151
+ versions we are testing against).
85
152
 
86
153
  ## Installation
87
154
 
@@ -105,53 +172,133 @@ $ gem install fortnox-api
105
172
 
106
173
  # Usage
107
174
 
108
- ## Getting an AccessToken
109
- To make calls to the API server you need a `ClientSecret` and an `AccessToken`. When you sign up for an API-account with Fortnox you should get a client secret and an authorization code. To get the access token, that is reusable, you need to do a one time exchange with the API-server and exchange your authorization code for an access token. For more information about how to get access tokens, see Fortnox developer documentation.
110
-
111
- ## Configuration
112
- To configure the gem you can use the `configure` block. A `client_secret` and `access_token` (or `access_tokens` in plural, see [Multiple AccessTokens](#multiple-accesstokens)) are required configurations for the gem to work so at the very minimum you will need something like:
175
+ ## Authorization
176
+
177
+ > :warning: Before 2022, Fortnox used a client ID and a fixed access token for
178
+ > authorization. This way of is now deprecated. The old access tokens have a
179
+ > life span of 10 years according to Fortnox. They can still be used, but you
180
+ > can't issue any new long lived tokens and they recommend to migrate to the new
181
+ > authorization process. This gem will no longer support the old way of
182
+ > authorization since v0.9.0.
183
+
184
+ You need to have a Fortnox app and to create such an app, you need to register
185
+ as a Fortnox developer. It might feel as if "I just want to create an
186
+ integration to Fortnox, not build a public app to in the marketplace". Yeah, we
187
+ agree... You don't need to release the app on the Fortnox Marketplace, but you
188
+ need that Fortnox app. Also, see further Fortnox app requirements down below.
189
+
190
+ Start your journey at
191
+ [Fortnox getting started guide](https://developer.fortnox.se/getting-started/).
192
+ Note that there's a script to authorize the Fortnox app to your Fortnox account
193
+ bundled with this gem to help you getting started, see
194
+ [Initialization](#initialization). Also read
195
+ [Authorizing your integration](https://developer.fortnox.se/general/authentication/).
196
+
197
+ Things you need:
198
+
199
+ - A Fortnox developer account
200
+ - A Fortnox app with:
201
+ - Service account setting enabled (it's used in server to server integrations,
202
+ which this is)
203
+ - Correct scopes set
204
+ - A redirect URL (just use a dummy URL if you want to, you just need the
205
+ parameters send to that URL)
206
+ - A Fortnox test environment so that you can test your integration.
207
+
208
+ When you have authorized your integration you get an access token from Fortnox.
209
+ It's a JWT with a expiration time (currently **1 hour**). You also get a long
210
+ lived refresh token (currently lasts for **31 days** ). When you need a new
211
+ access token you send a renewal request to Fortnox. That request contains the
212
+ new access token as well as a new refresh token and some other data. Note that
213
+ **the old refresh token is invalidated when new tokens are requested**. As long
214
+ as you have a valid refresh token you will be available to request new tokens.
215
+
216
+ The gem exposes a specific repository for renewing tokens. You use it like this:
113
217
 
114
218
  ```ruby
115
- Fortnox::API.configure do |config|
116
- config.client_secret = 'P5K5wE3Kun'
117
- config.access_token = '3f08d038-f380-4893-94a0a0-8f6e60e67a'
118
- end
219
+ require 'fortnox/api'
220
+
221
+ tokens = Fortnox::API::Repository::Authentication.new.renew_tokens(
222
+ refresh_token: 'a valid refresh token',
223
+ client_id: "the integration's client id",
224
+ client_secret: "the integration's client secret"
225
+ )
226
+
227
+ # You probably want to persist your tokens somehow...
228
+ store_refresh_token(tokens[:refresh_token])
229
+ store_access_token(tokens[:access_token])
230
+
231
+ # Set the new access token
232
+ Fortnox::API.access_token = tokens[:access_token]
233
+
234
+ # The gem will now use the new access token
235
+ Fortnox::API::Repository::Customer.new.all
119
236
  ```
120
- Before you start using the gem.
121
237
 
122
- ### Multiple AccessTokens
238
+ It's up to you to provide a valid token to the gem and to renew it regularly,
239
+ otherwise you need to start over again with the
240
+ [Initialization](#initialization).
241
+
242
+ ## Get tokens
243
+
244
+ There's a script in `bin/get_tokens` to issue valid access and refresh tokens.
245
+ Provide valid credentials in `.env`, see `.env.template` or have a look in the
246
+ script itself to see what's needed.
247
+
248
+ ### Configuration
249
+
250
+ The gem can be configured in a `configure` block, where `setting` is one of the
251
+ settings from the table below.
123
252
 
124
- Fortnox uses quite low [API rate limits](https://developer.fortnox.se/blog/important-implementation-of-rate-limits/). The limit is for each access token, and according to Fortnox you can use as many tokens as you like to get around this problem. This gem supports handeling multiple access tokens natively. Just set the `access_tokens` (in plural, compared to `access_token` that only takes a String) to a list of strings:
125
253
  ```ruby
126
254
  Fortnox::API.configure do |config|
127
- config.client_secret = 'P5K5wE3Kun'
128
- config.access_tokens ['a78d35hc-j5b1-ga1b-a1h6-h72n74fj5327', 's2b45f67-dh5d-3g5s-2dj5-dku6gn26sh62']
255
+ config.setting = 'value'
129
256
  end
130
257
  ```
131
- The gem will then automatically rotate between these tokens. In theory you can declare as many as you like. Remember that you will need to use one authorization code to get each token! See Fortnox developer documentation for more information about how to get access tokens.
132
258
 
133
- ### AccessTokens for multiple Fortnox accounts
134
- Yes, we support working with several accounts at once as well. Simply set `access_tokens` to a hash where the keys (called a *token store*) represents different fortnox accounts and the value(s) for a specific key is an array or a string with access token(s) linked to that specific Fortnox account. For instance: `{ account1: ['token1', 'token2'], account2: 'token2' }`. If you provide a `:default` token store, this is used as default by all repositories.
259
+ | Setting | Description | Required | Default |
260
+ | ----------- | --------------------------------- | -------- | --------------------------------------------------------------- |
261
+ | `base_url` | The base url to Fortnox API | No | `'https://api.fortnox.se/3/'` |
262
+ | `token_url` | The url to Fortnox token endpoint | No | `'https://apps.fortnox.se/oauth-v1/token'` |
263
+ | `debugging` | For debugging | No | `false` |
264
+ | `logger` | The logger to use | No | A simple logger that writes to `$stdout` with log level `WARN`. |
265
+
266
+ ### Support for multiple Fortnox accounts
267
+
268
+ Yes, we support working with several accounts at once. Simply switch access
269
+ token between calls. The token is stored in the current thread, so it's thread
270
+ safe.
135
271
 
136
272
  ```ruby
137
- Fortnox::API.configure do |config|
138
- config.client_secret = 'P5K5wE3Kun'
139
- config.access_tokens = {
140
- default: ['3f08d038-f380-4893-94a0a0-8f6e60e67a', 'a78d35hc-j5b1-ga1b-a1h6-h72n74fj5327'],
141
- another_account: ['s2b45f67-dh5d-3g5s-2dj5-dku6gn26sh62']
142
- }
143
- end
273
+ repository = Fortnox::API::Repository::Customer.new
144
274
 
145
- Fortnox::API::Repository::Customer.new # Using token store :default
146
- Fortnox::API::Repository::Customer.new( token_store: :another_account ) # Using token store :another_account
275
+ Fortnox::API.access_token = 'account1_access_token'
276
+ repository.all # Calls account1
277
+
278
+ Fortnox::API.access_token = 'account2_access_token'
279
+ repository.all # Calls account2
147
280
  ```
148
- The tokens per store are rotated between calls to the backend as well. That way you can create a web app that connects to multiple Fortnox accounts and uses multiple tokens for each account as well.
281
+
282
+ ### Automatic access tokens rotation (deprecated)
283
+
284
+ As of november 2021 and the new OAuth 2 flow, Fortnox has made
285
+ [adjustments to the rate limit](https://developer.fortnox.se/blog/adjustments-to-the-rate-limit/)
286
+ and it is no longer calculated per access token (if you are not using the old
287
+ auth flow, but that flow is deprecated in this gem since v0.9.0).
149
288
 
150
289
  # Usage
290
+
151
291
  ## Repositories
152
- Repositories are used to load,save and remove entities from the remote server. The calls are subject to network latency and are blocking. Do make sure to rescue appropriate network errors in your code.
292
+
293
+ Repositories are used to load,save and remove entities from the remote server.
294
+ The calls are subject to network latency and are blocking. Do make sure to
295
+ rescue appropriate network errors in your code.
153
296
 
154
297
  ```ruby
298
+ require 'fortnox/api'
299
+
300
+ Fortnox::API.access_token = 'valid_access_token'
301
+
155
302
  # Instanciate a repository
156
303
  repo = Fortnox::API::Repository::Customer.new
157
304
 
@@ -164,22 +311,38 @@ repo.find( 5 ) #=> <Fortnox::API::Model::Customer:0x007fdf21100b00>
164
311
  # Get entities by attribute
165
312
  repo.find_by( customer_number: 5 ) #=> <Fortnox::API::Collection:0x007fdf22994310 @entities: [<Fortnox::API::Customer::Simple:0x007fdf22949298>]
166
313
  ```
167
- If you are eagle eyed you might have spotted the different classes for the entities returned in a collection vs the one we get from find. The `Simple` version of a class is used in thouse cases where the API-server doesn't return a full set of attributes for an entity. For customers the simple version has 10 attributes while the full have over 40.
168
314
 
169
- > ​:info: ** Collections not implemented yet.
315
+ If you are eagle eyed you might have spotted the different classes for the
316
+ entities returned in a collection vs the one we get from find. The `Simple`
317
+ version of a class is used in thouse cases where the API-server doesn't return a
318
+ full set of attributes for an entity. For customers the simple version has 10
319
+ attributes while the full have over 40.
320
+
321
+ > :info: \*\* Collections not implemented yet.
170
322
 
171
- You should try to get by using the simple versions for as long as possible. Both the `Collection` and `Simple` classes have a `.full` method that will give you full versions of the entities. Bare in mind though that a collection of 20 simple models that you run `.full` on will call out to the server 20 times, in sequence.
323
+ You should try to get by using the simple versions for as long as possible. Both
324
+ the `Collection` and `Simple` classes have a `.full` method that will give you
325
+ full versions of the entities. Bare in mind though that a collection of 20
326
+ simple models that you run `.full` on will call out to the server 20 times, in
327
+ sequence.
172
328
 
173
- > ​:info: ** We have opened a dialog with Fortnox about this API practice to allow for full models in the list request, on demand, and/or the ability for the client to specify the fields of interest when making the request, as per usual in REST APIs with partial load.
329
+ > :info: \*\* We have opened a dialog with Fortnox about this API practice to
330
+ > allow for full models in the list request, on demand, and/or the ability for
331
+ > the client to specify the fields of interest when making the request, as per
332
+ > usual in REST APIs with partial load.
174
333
 
175
334
  ## Entities
176
- All the repository methods return instances or collections of instances of some resource
177
- class such as customer, invoice, item, voucher and so on.
335
+
336
+ All the repository methods return instances or collections of instances of some
337
+ resource class such as customer, invoice, item, voucher and so on.
178
338
 
179
339
  Instances are immutable and any update returns a new instance with the
180
- appropriate attributes changed (see the Immutable section under Architecture above for more details). To change the properties of a model works like this:
340
+ appropriate attributes changed (see the Immutable section under Architecture
341
+ above for more details). To change the properties of a model works like this:
181
342
 
182
343
  ```ruby
344
+ require 'fortnox/api'
345
+
183
346
  customer #=> <Fortnox::API::Model::Customer:0x007fdf228db310>
184
347
  customer.name #=> "Nelly Bloom"
185
348
  customer.update( name: "Ned Stark" ) #=> <Fortnox::API::Model::Customer:0x0193a456ff0307>
@@ -189,7 +352,9 @@ updated_customer = customer.update( name: "Ned Stark" ) #=> <Fortnox::API::Model
189
352
  updated_customer.name #=> "Ned Stark"
190
353
  ```
191
354
 
192
- The update method takes an implicit hash of attributes to update, so you can update as many as you like in one go.
355
+ The update method takes an implicit hash of attributes to update, so you can
356
+ update as many as you like in one go.
193
357
 
194
358
  # Contributing
359
+
195
360
  See the [CONTRIBUTE](CONTRIBUTE.md) readme.
data/Rakefile CHANGED
@@ -1,7 +1,140 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ # rubocop:disable Metrics/MethodLength
4
+
3
5
  require 'rspec/core/rake_task'
6
+ require 'dotenv'
7
+
8
+ require_relative 'lib/fortnox/api'
9
+
10
+ Dotenv.load('.env.test')
4
11
 
5
12
  RSpec::Core::RakeTask.new(:spec)
6
13
 
7
14
  task default: :spec
15
+
16
+ desc 'Remove all VCR cassettes so we can rerecord them'
17
+ task :throw_vcr_cassettes do
18
+ FileUtils.rm_rf(Dir.glob('spec/vcr_cassettes/**'))
19
+ end
20
+
21
+ desc 'Seed Fortnox test instane with data required for the test suite'
22
+ task :seed_fortnox_test_instance do
23
+ Fortnox::API.configure do |config|
24
+ config.client_id = ENV.fetch('FORTNOX_API_CLIENT_ID')
25
+ config.client_secret = ENV.fetch('FORTNOX_API_CLIENT_SECRET')
26
+ config.storage = Class.new do
27
+ def access_token
28
+ ENV.fetch('FORTNOX_API_ACCESS_TOKEN')
29
+ end
30
+
31
+ def refresh_token; end
32
+
33
+ def access_token=(token); end
34
+
35
+ def refresh_token=(token); end
36
+ end.new
37
+ end
38
+
39
+ puts 'Seeting Fortnox test instance account with data required for testing...'
40
+
41
+ seed_customer_data
42
+ seed_article_data
43
+ seed_invoice_data
44
+ seed_order_data
45
+
46
+ puts 'Done'
47
+ end
48
+
49
+ def seed_customer_data
50
+ customer_repository = Fortnox::API::Repository::Customer.new
51
+
52
+ customer_repository.save(
53
+ Fortnox::API::Model::Customer.new(
54
+ name: 'A customer from New York',
55
+ city: 'New York'
56
+ )
57
+ )
58
+ customer_repository.save(
59
+ Fortnox::API::Model::Customer.new(
60
+ name: 'Another customer from New York',
61
+ city: 'New York',
62
+ zip_code: '10001'
63
+ )
64
+ )
65
+ end
66
+
67
+ def seed_article_data
68
+ article_repository = Fortnox::API::Repository::Article.new
69
+
70
+ article_repository.save(
71
+ Fortnox::API::Model::Article.new(
72
+ article_number: 101,
73
+ description: 'Hammer'
74
+ )
75
+ )
76
+
77
+ article_repository.save(
78
+ Fortnox::API::Model::Article.new(
79
+ article_number: 102,
80
+ description: 'Hammer'
81
+ )
82
+ )
83
+
84
+ article_repository.save(
85
+ Fortnox::API::Model::Article.new(
86
+ description: 'Test article'
87
+ )
88
+ )
89
+
90
+ article_repository.save(
91
+ Fortnox::API::Model::Article.new(
92
+ description: 'Test article'
93
+ )
94
+ )
95
+ end
96
+
97
+ # TODO: When we have support for actions, we should set the states for
98
+ # Invoices required by the Invoice Repository spec.
99
+ def seed_invoice_data
100
+ invoice_repository = Fortnox::API::Repository::Invoice.new
101
+
102
+ invoice_repository.save(
103
+ Fortnox::API::Model::Invoice.new(
104
+ customer_number: '1',
105
+ your_reference: 'Gandalf the Grey'
106
+ )
107
+ )
108
+
109
+ invoice_repository.save(
110
+ Fortnox::API::Model::Invoice.new(
111
+ customer_number: '1',
112
+ your_reference: 'Gandalf the Grey',
113
+ our_reference: 'Radagast the Brown'
114
+ )
115
+ )
116
+ end
117
+
118
+ # TODO: When we have support for actions, we should set the states for
119
+ # Order required by the Order Repository spec.
120
+ # Also, we should create Orders needed for search tests.
121
+ def seed_order_data
122
+ order_repository = Fortnox::API::Repository::Order.new
123
+
124
+ order_repository.save(
125
+ Fortnox::API::Model::Order.new(
126
+ customer_number: '1',
127
+ our_reference: 'Belladonna Took'
128
+ )
129
+ )
130
+
131
+ order_repository.save(
132
+ Fortnox::API::Model::Order.new(
133
+ customer_number: '1',
134
+ our_reference: 'Belladonna Took',
135
+ your_reference: 'Bodo Proudfoot'
136
+ )
137
+ )
138
+ end
139
+
140
+ # rubocop:enable Metrics/MethodLength
data/bin/get_tokens ADDED
@@ -0,0 +1,79 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require 'base64'
5
+ require 'dotenv'
6
+ require 'httparty'
7
+
8
+ DOTENV_FILE_NAME = '.env'
9
+ OAUTH_ENDPOINT = 'https://apps.fortnox.se/oauth-v1'
10
+
11
+ Dotenv.load(DOTENV_FILE_NAME)
12
+
13
+ ERROR_MESSAGE = 'Missing environment variabel %s! Exiting.'
14
+ CLIENT_ID = ENV.fetch('FORTNOX_API_CLIENT_ID') { |key| raise(format(ERROR_MESSAGE, key)) }
15
+ CLIENT_SECRET = ENV.fetch('FORTNOX_API_CLIENT_SECRET') { |key| raise(format(ERROR_MESSAGE, key)) }
16
+ REDIRECT_URI = ENV.fetch('FORTNOX_API_REDIRECT_URI') { |key| raise(format(ERROR_MESSAGE, key)) }
17
+ SCOPES = ENV.fetch('FORTNOX_API_SCOPES') { |key| raise(format(ERROR_MESSAGE, key)) }
18
+ CREDENTIALS = Base64.encode64("#{CLIENT_ID}:#{CLIENT_SECRET}").to_s
19
+
20
+ AUTH_PARAMS =
21
+ "client_id=#{CGI.escape(CLIENT_ID)}" \
22
+ "&redirect_uri=#{CGI.escape(REDIRECT_URI)}" \
23
+ "&scope=#{CGI.escape(SCOPES)}" \
24
+ '&state=not-used' \
25
+ '&access_type=offline' \
26
+ '&response_type=code' \
27
+ '&account_type=service'
28
+
29
+ AUTHORIZE_URL = "#{OAUTH_ENDPOINT}/auth?#{AUTH_PARAMS}"
30
+
31
+ print "Let's get you an access token and a refresh token for Fortnox API."
32
+ print "\nVisit the URL below, login with the account you want to authorize and grant the integration permission." \
33
+ 'Then copy the "code" from the URL parameters.'
34
+ print "\n#{AUTHORIZE_URL}"
35
+
36
+ print "\nEnter authorization code: "
37
+ authorization_code = $stdin.gets.chomp
38
+
39
+ headers = {
40
+ Authorization: "Basic #{CREDENTIALS}",
41
+ 'Content-type' => 'application/x-www-form-urlencoded'
42
+ }
43
+
44
+ body = {
45
+ grant_type: 'authorization_code',
46
+ code: authorization_code,
47
+ redirect_uri: REDIRECT_URI
48
+ }
49
+
50
+ response = HTTParty.post("#{OAUTH_ENDPOINT}/token", headers: headers, body: body)
51
+
52
+ if response.code != 200
53
+ print "\nSomething went wrong."
54
+ print "\nResponse code #{response.code}"
55
+ print "\nResponse message: #{response.message}"
56
+ print "\nResponse body: #{response.body}"
57
+ print "\n"
58
+ exit
59
+ end
60
+
61
+ access_token = response.parsed_response['access_token']
62
+ refresh_token = response.parsed_response['refresh_token']
63
+
64
+ print "There you go, here's your access token: #{access_token}"
65
+ print "\nAnd here's your refresh token: #{refresh_token}"
66
+ print "\nYour access token expires in #{response.parsed_response['expires_in']} seconds"
67
+ print "\nRequested scopes: #{response.parsed_response['scope']}"
68
+ print "\n"
69
+
70
+ print "Write tokens to #{DOTENV_FILE_NAME} [y/n]? "
71
+ store_tokens_in_env = $stdin.gets.chomp
72
+
73
+ exit unless store_tokens_in_env.casecmp('y').zero?
74
+
75
+ text = File.read(DOTENV_FILE_NAME)
76
+ updated_text = text
77
+ .gsub(/FORTNOX_API_ACCESS_TOKEN=.*$/, "FORTNOX_API_ACCESS_TOKEN=#{access_token}")
78
+ .gsub(/FORTNOX_API_REFRESH_TOKEN=.*$/, "FORTNOX_API_REFRESH_TOKEN=#{refresh_token}")
79
+ File.write(DOTENV_FILE_NAME, updated_text)
data/bin/renew_tokens ADDED
@@ -0,0 +1,28 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require 'bundler/setup'
5
+ require 'fortnox/api'
6
+ require 'dotenv'
7
+
8
+ DOTENV_FILE_NAME = '.env'
9
+ Dotenv.load(DOTENV_FILE_NAME)
10
+
11
+ tokens = Fortnox::API::Repository::Authentication.new.renew_tokens(
12
+ refresh_token: ENV.fetch('FORTNOX_API_REFRESH_TOKEN'),
13
+ client_id: ENV.fetch('FORTNOX_API_CLIENT_ID'),
14
+ client_secret: ENV.fetch('FORTNOX_API_CLIENT_SECRET')
15
+ )
16
+
17
+ puts tokens
18
+
19
+ print "Write tokens to #{DOTENV_FILE_NAME} [y/n]? "
20
+ store_tokens_in_env = $stdin.gets.chomp
21
+
22
+ exit unless store_tokens_in_env.casecmp('y').zero?
23
+
24
+ text = File.read(DOTENV_FILE_NAME)
25
+ updated_text = text
26
+ .gsub(/FORTNOX_API_ACCESS_TOKEN=.*$/, "FORTNOX_API_ACCESS_TOKEN=#{tokens[:access_token]}")
27
+ .gsub(/FORTNOX_API_REFRESH_TOKEN=.*$/, "FORTNOX_API_REFRESH_TOKEN=#{tokens[:refresh_token]}")
28
+ File.write(DOTENV_FILE_NAME, updated_text)