fortnox-api 0.8.2 → 0.9.0

Sign up to get free protection for your applications and to get access to all the features.
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/README.md CHANGED
@@ -1,86 +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 December 2020):
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 hope to be able to continue with our work with [rest_easy gem](https://github.com/accodeing/rest_easy), which generalize REST API's in general.
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.
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.
20
35
 
21
36
  # Architecture overview
22
- 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.
23
37
 
24
- 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.
25
40
 
26
- 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.
27
44
 
28
- 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/)
29
57
 
30
58
  ## Model
31
- 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.
32
63
 
33
64
  ### Attribute
34
- 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.
35
70
 
36
71
  ### Immutability
72
+
37
73
  The model instances are immutable. That means:
74
+
38
75
  ```ruby
39
76
  customer.name # => "Old Name"
40
77
  customer.name = 'New Name' # => "New Name"
41
78
 
42
79
  customer.name == "New Name" # => false
43
80
  ```
44
- 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.
45
86
 
46
87
  So you might think you should do this instead:
88
+
47
89
  ```ruby
48
90
  customer = customer.name = 'New Name' # => "New Name"
49
91
  ```
50
- 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
+
51
99
  ```ruby
52
100
  customer.name # => "Old Name"
53
101
  updated_customer = customer.update( name: 'New Name' ) # => <Fortnox::API::Model::Customer:0x007fdf22949298 ... >
54
102
  updated_customer.name == "New Name" # => true
55
103
  ```
104
+
56
105
  And note that:
106
+
57
107
  ```ruby
58
108
  customer.name # => "Old Name"
59
109
  customer.update( name: 'New Name' ) # => <Fortnox::API::Model::Customer:0x007fdf21100b00 ... >
60
110
  customer.name == "New Name" # => false
61
111
  ```
112
+
62
113
  This is how all the models work, they are all immutable.
63
114
 
64
115
  ### Exceptions
65
116
 
66
- 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.
67
121
 
68
122
  ## Type
69
- 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).
70
130
 
71
131
  ## Repositories
72
- 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.
73
135
 
74
136
  ### Exceptions
75
137
 
76
- 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.
77
140
 
78
141
  ## Mappers
79
- 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.
80
147
 
81
148
  # Requirements
82
149
 
83
- This gem is built for Ruby 2.5 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).
84
152
 
85
153
  ## Installation
86
154
 
@@ -104,53 +172,133 @@ $ gem install fortnox-api
104
172
 
105
173
  # Usage
106
174
 
107
- ## Getting an AccessToken
108
- 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.
109
-
110
- ## Configuration
111
- 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:
112
217
 
113
218
  ```ruby
114
- Fortnox::API.configure do |config|
115
- config.client_secret = 'P5K5wE3Kun'
116
- config.access_token = '3f08d038-f380-4893-94a0a0-8f6e60e67a'
117
- 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
118
236
  ```
119
- Before you start using the gem.
120
237
 
121
- ### 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.
122
252
 
123
- 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:
124
253
  ```ruby
125
254
  Fortnox::API.configure do |config|
126
- config.client_secret = 'P5K5wE3Kun'
127
- config.access_tokens ['a78d35hc-j5b1-ga1b-a1h6-h72n74fj5327', 's2b45f67-dh5d-3g5s-2dj5-dku6gn26sh62']
255
+ config.setting = 'value'
128
256
  end
129
257
  ```
130
- 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.
131
258
 
132
- ### AccessTokens for multiple Fortnox accounts
133
- 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.
134
271
 
135
272
  ```ruby
136
- Fortnox::API.configure do |config|
137
- config.client_secret = 'P5K5wE3Kun'
138
- config.access_tokens = {
139
- default: ['3f08d038-f380-4893-94a0a0-8f6e60e67a', 'a78d35hc-j5b1-ga1b-a1h6-h72n74fj5327'],
140
- another_account: ['s2b45f67-dh5d-3g5s-2dj5-dku6gn26sh62']
141
- }
142
- end
273
+ repository = Fortnox::API::Repository::Customer.new
274
+
275
+ Fortnox::API.access_token = 'account1_access_token'
276
+ repository.all # Calls account1
143
277
 
144
- Fortnox::API::Repository::Customer.new # Using token store :default
145
- Fortnox::API::Repository::Customer.new( token_store: :another_account ) # Using token store :another_account
278
+ Fortnox::API.access_token = 'account2_access_token'
279
+ repository.all # Calls account2
146
280
  ```
147
- 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).
148
288
 
149
289
  # Usage
290
+
150
291
  ## Repositories
151
- 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.
152
296
 
153
297
  ```ruby
298
+ require 'fortnox/api'
299
+
300
+ Fortnox::API.access_token = 'valid_access_token'
301
+
154
302
  # Instanciate a repository
155
303
  repo = Fortnox::API::Repository::Customer.new
156
304
 
@@ -163,22 +311,38 @@ repo.find( 5 ) #=> <Fortnox::API::Model::Customer:0x007fdf21100b00>
163
311
  # Get entities by attribute
164
312
  repo.find_by( customer_number: 5 ) #=> <Fortnox::API::Collection:0x007fdf22994310 @entities: [<Fortnox::API::Customer::Simple:0x007fdf22949298>]
165
313
  ```
166
- 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.
167
314
 
168
- > ​: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.
169
320
 
170
- 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.
321
+ > :info: \*\* Collections not implemented yet.
171
322
 
172
- > ​: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.
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.
328
+
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.
173
333
 
174
334
  ## Entities
175
- All the repository methods return instances or collections of instances of some resource
176
- 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.
177
338
 
178
339
  Instances are immutable and any update returns a new instance with the
179
- 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:
180
342
 
181
343
  ```ruby
344
+ require 'fortnox/api'
345
+
182
346
  customer #=> <Fortnox::API::Model::Customer:0x007fdf228db310>
183
347
  customer.name #=> "Nelly Bloom"
184
348
  customer.update( name: "Ned Stark" ) #=> <Fortnox::API::Model::Customer:0x0193a456ff0307>
@@ -188,11 +352,9 @@ updated_customer = customer.update( name: "Ned Stark" ) #=> <Fortnox::API::Model
188
352
  updated_customer.name #=> "Ned Stark"
189
353
  ```
190
354
 
191
- The update method takes an implicit hash of attributes to update, so you can update as many as you like in one go.
192
-
193
- # Development
194
- ## Testing
195
- This gem has integration tests to verify the code against the real API. It uses [vcr](https://github.com/vcr/vcr) to record API endpoint responses. These responses are stored locally and are called vcr cassettes. If no cassettes are available, vcr will record new ones for you. Once in a while, it's good to throw away all cassettes and rerecord them. Fortnox updates their endpoints and we need to keep our code up to date with the reality. There's a handy rake task for removing all cassettes, see `rake -T`. Note that when rerecording all cassettes, do it one repository at a time, otherwise you'll definitely get `429 Too Many Requests` from Fortnox. Run them manually with something like `bundle exec rspec spec/fortnox/api/repositories/article_spec.rb`. Also, you will need to update some test data in specs, see notes in specs.
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.
196
357
 
197
358
  # Contributing
359
+
198
360
  See the [CONTRIBUTE](CONTRIBUTE.md) readme.
data/Rakefile CHANGED
@@ -1,6 +1,13 @@
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
 
@@ -10,3 +17,124 @@ desc 'Remove all VCR cassettes so we can rerecord them'
10
17
  task :throw_vcr_cassettes do
11
18
  FileUtils.rm_rf(Dir.glob('spec/vcr_cassettes/**'))
12
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)