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.
- checksums.yaml +4 -4
- data/.codeclimate.yml +1 -0
- data/.env.template +7 -0
- data/.env.test +11 -3
- data/.gitignore +7 -1
- data/.rubocop.yml +18 -2
- data/.tool-versions +1 -0
- data/.travis.yml +15 -19
- data/CHANGELOG.md +66 -1
- data/CONTRIBUTE.md +21 -9
- data/DEVELOPER_README.md +72 -0
- data/Guardfile +13 -4
- data/README.md +226 -61
- data/Rakefile +133 -0
- data/bin/get_tokens +79 -0
- data/bin/renew_tokens +28 -0
- data/fortnox-api.gemspec +31 -25
- data/lib/fortnox/api/mappers/article.rb +1 -1
- data/lib/fortnox/api/mappers/base/from_json.rb +7 -6
- data/lib/fortnox/api/mappers/base/to_json.rb +4 -5
- data/lib/fortnox/api/mappers/base.rb +3 -3
- data/lib/fortnox/api/mappers/customer.rb +1 -1
- data/lib/fortnox/api/mappers/default_delivery_types.rb +1 -1
- data/lib/fortnox/api/mappers/default_templates.rb +1 -1
- data/lib/fortnox/api/mappers/edi_information.rb +1 -1
- data/lib/fortnox/api/mappers/email_information.rb +1 -1
- data/lib/fortnox/api/mappers/invoice.rb +4 -4
- data/lib/fortnox/api/mappers/invoice_row.rb +1 -1
- data/lib/fortnox/api/mappers/order.rb +4 -4
- data/lib/fortnox/api/mappers/order_row.rb +1 -1
- data/lib/fortnox/api/mappers/project.rb +1 -1
- data/lib/fortnox/api/mappers/terms_of_payment.rb +1 -1
- data/lib/fortnox/api/mappers/unit.rb +1 -1
- data/lib/fortnox/api/mappers/value/country_string.rb +1 -1
- data/lib/fortnox/api/mappers.rb +18 -18
- data/lib/fortnox/api/models/article.rb +2 -2
- data/lib/fortnox/api/models/base.rb +23 -21
- data/lib/fortnox/api/models/customer.rb +57 -57
- data/lib/fortnox/api/models/document.rb +5 -2
- data/lib/fortnox/api/models/invoice.rb +2 -2
- data/lib/fortnox/api/models/label.rb +3 -3
- data/lib/fortnox/api/models/order.rb +2 -2
- data/lib/fortnox/api/models/project.rb +2 -2
- data/lib/fortnox/api/models/terms_of_payment.rb +2 -2
- data/lib/fortnox/api/models/unit.rb +2 -2
- data/lib/fortnox/api/models.rb +7 -7
- data/lib/fortnox/api/repositories/article.rb +3 -3
- data/lib/fortnox/api/repositories/authentication.rb +61 -0
- data/lib/fortnox/api/repositories/base/savers.rb +3 -1
- data/lib/fortnox/api/repositories/base.rb +25 -38
- data/lib/fortnox/api/repositories/customer.rb +3 -3
- data/lib/fortnox/api/repositories/invoice.rb +3 -3
- data/lib/fortnox/api/repositories/order.rb +3 -3
- data/lib/fortnox/api/repositories/project.rb +3 -3
- data/lib/fortnox/api/repositories/terms_of_payment.rb +3 -3
- data/lib/fortnox/api/repositories/unit.rb +3 -3
- data/lib/fortnox/api/repositories.rb +8 -7
- data/lib/fortnox/api/request_handling.rb +30 -18
- data/lib/fortnox/api/types/default_delivery_types.rb +0 -2
- data/lib/fortnox/api/types/default_templates.rb +0 -2
- data/lib/fortnox/api/types/document_row.rb +3 -3
- data/lib/fortnox/api/types/edi_information.rb +0 -2
- data/lib/fortnox/api/types/email_information.rb +0 -2
- data/lib/fortnox/api/types/enums.rb +54 -10
- data/lib/fortnox/api/types/invoice_row.rb +1 -1
- data/lib/fortnox/api/types/model.rb +5 -9
- data/lib/fortnox/api/types/nullable.rb +13 -9
- data/lib/fortnox/api/types/order_row.rb +1 -1
- data/lib/fortnox/api/types/required.rb +3 -3
- data/lib/fortnox/api/types/sized.rb +4 -4
- data/lib/fortnox/api/types.rb +37 -24
- data/lib/fortnox/api/version.rb +1 -1
- data/lib/fortnox/api.rb +21 -39
- data/spec/fortnox/api/mappers/base/canonical_name_sym_spec.rb +13 -11
- data/spec/fortnox/api/mappers/base/from_json_spec.rb +10 -12
- data/spec/fortnox/api/mappers/base/to_json_spec.rb +48 -57
- data/spec/fortnox/api/mappers/base_spec.rb +4 -7
- data/spec/fortnox/api/mappers/contexts/json_conversion.rb +38 -33
- data/spec/fortnox/api/mappers/default_delivery_types_spec.rb +1 -1
- data/spec/fortnox/api/mappers/examples/mapper.rb +1 -1
- data/spec/fortnox/api/mappers/unit_spec.rb +3 -4
- data/spec/fortnox/api/models/base_spec.rb +33 -22
- data/spec/fortnox/api/models/unit_spec.rb +5 -3
- data/spec/fortnox/api/repositories/article_spec.rb +14 -9
- data/spec/fortnox/api/repositories/authentication_spec.rb +103 -0
- data/spec/fortnox/api/repositories/base_spec.rb +105 -326
- data/spec/fortnox/api/repositories/customer_spec.rb +37 -7
- data/spec/fortnox/api/repositories/examples/all.rb +0 -1
- data/spec/fortnox/api/repositories/examples/find.rb +5 -8
- data/spec/fortnox/api/repositories/examples/only.rb +4 -13
- data/spec/fortnox/api/repositories/examples/save.rb +32 -18
- data/spec/fortnox/api/repositories/examples/save_with_nested_model.rb +0 -5
- data/spec/fortnox/api/repositories/examples/save_with_specially_named_attribute.rb +1 -4
- data/spec/fortnox/api/repositories/examples/search.rb +4 -7
- data/spec/fortnox/api/repositories/invoice_spec.rb +72 -29
- data/spec/fortnox/api/repositories/order_spec.rb +11 -9
- data/spec/fortnox/api/repositories/project_spec.rb +7 -6
- data/spec/fortnox/api/repositories/terms_of_payment_spec.rb +9 -7
- data/spec/fortnox/api/repositories/unit_spec.rb +13 -11
- data/spec/fortnox/api/types/country_spec.rb +1 -1
- data/spec/fortnox/api/types/email_spec.rb +2 -2
- data/spec/fortnox/api/types/enums_spec.rb +1 -0
- data/spec/fortnox/api/types/examples/document_row.rb +3 -3
- data/spec/fortnox/api/types/examples/enum.rb +4 -4
- data/spec/fortnox/api/types/examples/types.rb +1 -3
- data/spec/fortnox/api/types/housework_types_spec.rb +124 -43
- data/spec/fortnox/api/types/model_spec.rb +13 -23
- data/spec/fortnox/api/types/nullable_spec.rb +30 -10
- data/spec/fortnox/api/types/order_row_spec.rb +2 -2
- data/spec/fortnox/api/types/required_spec.rb +7 -15
- data/spec/fortnox/api/types/sales_account_spec.rb +57 -0
- data/spec/fortnox/api_spec.rb +19 -124
- data/spec/spec_helper.rb +0 -14
- data/spec/support/helpers/configuration_helper.rb +30 -3
- data/spec/support/helpers.rb +1 -1
- data/spec/support/matchers/type/attribute_matcher.rb +2 -2
- data/spec/support/matchers/type/enum_matcher.rb +1 -1
- data/spec/support/matchers/type/have_account_number_matcher.rb +1 -1
- data/spec/support/matchers/type/have_email_matcher.rb +1 -1
- data/spec/support/matchers/type/have_nullable_date_matcher.rb +7 -5
- data/spec/support/matchers/type/have_nullable_matcher.rb +1 -1
- data/spec/support/matchers/type/have_nullable_string_matcher.rb +5 -5
- data/spec/support/matchers/type/require_attribute_matcher.rb +5 -5
- data/spec/support/matchers/type/type_matcher.rb +1 -1
- data/spec/support/vcr_setup.rb +16 -0
- data/spec/vcr_cassettes/articles/all.yml +41 -42
- data/spec/vcr_cassettes/articles/find_by_hash_failure.yml +36 -19
- data/spec/vcr_cassettes/articles/find_failure.yml +36 -19
- data/spec/vcr_cassettes/articles/find_id_1.yml +38 -20
- data/spec/vcr_cassettes/articles/find_new.yml +39 -22
- data/spec/vcr_cassettes/articles/multi_param_find_by_hash.yml +38 -21
- data/spec/vcr_cassettes/articles/save_new.yml +37 -19
- data/spec/vcr_cassettes/articles/save_old.yml +39 -22
- data/spec/vcr_cassettes/articles/save_with_specially_named_attribute.yml +37 -19
- data/spec/vcr_cassettes/articles/search_by_name.yml +41 -21
- data/spec/vcr_cassettes/articles/search_miss.yml +36 -19
- data/spec/vcr_cassettes/articles/search_with_special_char.yml +36 -19
- data/spec/vcr_cassettes/articles/single_param_find_by_hash.yml +38 -32
- data/spec/vcr_cassettes/authentication/expired_token.yml +54 -0
- data/spec/vcr_cassettes/authentication/invalid_authorization.yml +57 -0
- data/spec/vcr_cassettes/authentication/invalid_refresh_token.yml +58 -0
- data/spec/vcr_cassettes/authentication/valid_request.yml +63 -0
- data/spec/vcr_cassettes/customers/all.yml +44 -132
- data/spec/vcr_cassettes/customers/find_by_hash_failure.yml +36 -19
- data/spec/vcr_cassettes/customers/find_failure.yml +36 -19
- data/spec/vcr_cassettes/customers/find_id_1.yml +39 -21
- data/spec/vcr_cassettes/customers/find_new.yml +38 -21
- data/spec/vcr_cassettes/customers/find_with_sales_account.yml +63 -0
- data/spec/vcr_cassettes/customers/multi_param_find_by_hash.yml +38 -21
- data/spec/vcr_cassettes/customers/save_new.yml +36 -18
- data/spec/vcr_cassettes/customers/save_new_with_country_code_SE.yml +32 -24
- data/spec/vcr_cassettes/customers/save_new_with_sales_account.yml +63 -0
- data/spec/vcr_cassettes/customers/save_old.yml +38 -21
- data/spec/vcr_cassettes/customers/save_with_specially_named_attribute.yml +36 -18
- data/spec/vcr_cassettes/customers/search_by_name.yml +38 -48
- data/spec/vcr_cassettes/customers/search_miss.yml +36 -19
- data/spec/vcr_cassettes/customers/search_with_special_char.yml +36 -19
- data/spec/vcr_cassettes/customers/single_param_find_by_hash.yml +39 -22
- data/spec/vcr_cassettes/invoices/all.yml +71 -114
- data/spec/vcr_cassettes/invoices/filter_hit.yml +39 -24
- data/spec/vcr_cassettes/invoices/filter_invalid.yml +35 -17
- data/spec/vcr_cassettes/invoices/find_by_hash_failure.yml +36 -19
- data/spec/vcr_cassettes/invoices/find_failure.yml +36 -19
- data/spec/vcr_cassettes/invoices/find_id_1.yml +40 -22
- data/spec/vcr_cassettes/invoices/find_new.yml +41 -24
- data/spec/vcr_cassettes/invoices/multi_param_find_by_hash.yml +38 -21
- data/spec/vcr_cassettes/invoices/row_description_limit.yml +65 -0
- data/spec/vcr_cassettes/invoices/save_new.yml +39 -21
- data/spec/vcr_cassettes/invoices/save_new_with_comments.yml +39 -21
- data/spec/vcr_cassettes/invoices/save_new_with_country.yml +35 -26
- data/spec/vcr_cassettes/invoices/save_new_with_country_GB.yml +36 -27
- data/spec/vcr_cassettes/invoices/save_new_with_country_Norge.yml +35 -26
- data/spec/vcr_cassettes/invoices/save_new_with_country_Norway.yml +35 -26
- data/spec/vcr_cassettes/invoices/save_new_with_country_Sverige.yml +35 -26
- data/spec/vcr_cassettes/invoices/save_new_with_country_VA.yml +36 -27
- data/spec/vcr_cassettes/invoices/save_new_with_country_VI.yml +36 -27
- data/spec/vcr_cassettes/invoices/save_new_with_country_empty_string.yml +35 -26
- data/spec/vcr_cassettes/invoices/save_new_with_country_nil.yml +35 -26
- data/spec/vcr_cassettes/invoices/save_new_with_unsaved_parent.yml +65 -0
- data/spec/vcr_cassettes/invoices/save_old.yml +41 -24
- data/spec/vcr_cassettes/invoices/save_old_with_empty_comments.yml +41 -24
- data/spec/vcr_cassettes/invoices/save_old_with_empty_country.yml +37 -29
- data/spec/vcr_cassettes/invoices/save_old_with_nil_comments.yml +41 -24
- data/spec/vcr_cassettes/invoices/save_old_with_nil_country.yml +37 -29
- data/spec/vcr_cassettes/invoices/save_with_nested_model.yml +40 -21
- data/spec/vcr_cassettes/invoices/save_with_specially_named_attribute.yml +39 -20
- data/spec/vcr_cassettes/invoices/search_by_name.yml +38 -27
- data/spec/vcr_cassettes/invoices/search_miss.yml +36 -19
- data/spec/vcr_cassettes/invoices/search_with_special_char.yml +36 -19
- data/spec/vcr_cassettes/invoices/single_param_find_by_hash.yml +39 -22
- data/spec/vcr_cassettes/orders/all.yml +44 -119
- data/spec/vcr_cassettes/orders/filter_hit.yml +39 -26
- data/spec/vcr_cassettes/orders/filter_invalid.yml +35 -17
- data/spec/vcr_cassettes/orders/find_by_hash_failure.yml +36 -19
- data/spec/vcr_cassettes/orders/find_failure.yml +36 -19
- data/spec/vcr_cassettes/orders/find_id_1.yml +42 -23
- data/spec/vcr_cassettes/orders/find_new.yml +41 -24
- data/spec/vcr_cassettes/orders/housework_invalid_tax_reduction_type.yml +61 -0
- data/spec/vcr_cassettes/orders/housework_othercoses_invalid.yml +61 -0
- data/spec/vcr_cassettes/orders/housework_type_babysitting.yml +40 -21
- data/spec/vcr_cassettes/orders/housework_type_cleaning.yml +40 -21
- data/spec/vcr_cassettes/orders/housework_type_construction.yml +40 -21
- data/spec/vcr_cassettes/orders/housework_type_cooking.yml +36 -18
- data/spec/vcr_cassettes/orders/housework_type_electricity.yml +40 -21
- data/spec/vcr_cassettes/orders/housework_type_gardening.yml +40 -21
- data/spec/vcr_cassettes/orders/housework_type_glassmetalwork.yml +40 -21
- data/spec/vcr_cassettes/orders/housework_type_grounddrainagework.yml +40 -21
- data/spec/vcr_cassettes/orders/housework_type_hvac.yml +40 -21
- data/spec/vcr_cassettes/orders/housework_type_itservices.yml +65 -0
- data/spec/vcr_cassettes/orders/housework_type_majorappliancerepair.yml +65 -0
- data/spec/vcr_cassettes/orders/housework_type_masonry.yml +40 -21
- data/spec/vcr_cassettes/orders/housework_type_movingservices.yml +65 -0
- data/spec/vcr_cassettes/orders/housework_type_othercare.yml +40 -21
- data/spec/vcr_cassettes/orders/housework_type_othercosts.yml +40 -21
- data/spec/vcr_cassettes/orders/housework_type_paintingwallpapering.yml +40 -21
- data/spec/vcr_cassettes/orders/housework_type_snowplowing.yml +40 -21
- data/spec/vcr_cassettes/orders/housework_type_textileclothing.yml +40 -21
- data/spec/vcr_cassettes/orders/housework_type_tutoring.yml +36 -18
- data/spec/vcr_cassettes/orders/multi_param_find_by_hash.yml +38 -21
- data/spec/vcr_cassettes/orders/save_new.yml +40 -22
- data/spec/vcr_cassettes/orders/save_old.yml +41 -24
- data/spec/vcr_cassettes/orders/save_with_nested_model.yml +40 -21
- data/spec/vcr_cassettes/orders/search_by_name.yml +38 -23
- data/spec/vcr_cassettes/orders/search_miss.yml +36 -19
- data/spec/vcr_cassettes/orders/search_with_special_char.yml +36 -19
- data/spec/vcr_cassettes/orders/single_param_find_by_hash.yml +39 -22
- data/spec/vcr_cassettes/projects/all.yml +39 -37
- data/spec/vcr_cassettes/projects/find_by_hash_failure.yml +36 -19
- data/spec/vcr_cassettes/projects/find_failure.yml +36 -19
- data/spec/vcr_cassettes/projects/find_id_1.yml +38 -21
- data/spec/vcr_cassettes/projects/find_new.yml +39 -22
- data/spec/vcr_cassettes/projects/multi_param_find_by_hash.yml +40 -22
- data/spec/vcr_cassettes/projects/save_new.yml +37 -19
- data/spec/vcr_cassettes/projects/save_old.yml +39 -22
- data/spec/vcr_cassettes/projects/single_param_find_by_hash.yml +38 -21
- data/spec/vcr_cassettes/termsofpayments/all.yml +43 -29
- data/spec/vcr_cassettes/termsofpayments/find_failure.yml +36 -19
- data/spec/vcr_cassettes/termsofpayments/find_id_1.yml +38 -22
- data/spec/vcr_cassettes/termsofpayments/find_new.yml +38 -21
- data/spec/vcr_cassettes/termsofpayments/save_new.yml +37 -19
- data/spec/vcr_cassettes/termsofpayments/save_old.yml +38 -21
- data/spec/vcr_cassettes/units/all.yml +38 -26
- data/spec/vcr_cassettes/units/find_failure.yml +36 -19
- data/spec/vcr_cassettes/units/find_id_1.yml +38 -21
- data/spec/vcr_cassettes/units/find_new.yml +38 -21
- data/spec/vcr_cassettes/units/save_new.yml +37 -19
- data/spec/vcr_cassettes/units/save_old.yml +38 -21
- data/spec/vcr_cassettes/units/save_with_specially_named_attribute.yml +37 -19
- metadata +133 -252
- data/lib/fortnox/api/circular_queue.rb +0 -39
- data/spec/fortnox/api/circular_queue_spec.rb +0 -52
- data/spec/support/helpers/dummy_class_helper.rb +0 -38
- data/spec/support/helpers/when_performing_helper.rb +0 -7
- data/spec/vcr_cassettes/invoices/save_new_with_country_KR.yml +0 -57
- data/temp.txt +0 -1
data/README.md
CHANGED
@@ -1,87 +1,154 @@
|
|
1
1
|
# Fortnox API
|
2
|
-
|
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/
|
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
|
-
|
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
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
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
|
-
|
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
|
-
|
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
|
138
|
+
Repositories can throw `Fortnox::API::RemoteServerError` if something went wrong
|
139
|
+
at Fortnox.
|
78
140
|
|
79
141
|
## Mappers
|
80
|
-
|
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
|
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
|
-
##
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
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
|
-
|
116
|
-
|
117
|
-
|
118
|
-
|
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
|
-
|
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.
|
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
|
-
|
134
|
-
|
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.
|
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
|
146
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
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
|
-
>
|
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
|
-
|
177
|
-
|
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
|
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
|
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)
|