fortnox-api 1.0.0.rc1 → 1.0.0.rc2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +47 -0
- data/README.md +38 -27
- data/bin/fortnox-setup +19 -3
- data/lib/fortnox/resource.rb +10 -0
- data/lib/fortnox/resources/article.rb +1 -0
- data/lib/fortnox/resources/customer.rb +1 -0
- data/lib/fortnox/resources/invoice.rb +1 -0
- data/lib/fortnox/resources/label.rb +1 -0
- data/lib/fortnox/resources/order.rb +1 -0
- data/lib/fortnox/resources/project.rb +1 -0
- data/lib/fortnox/resources/terms_of_payment.rb +2 -1
- data/lib/fortnox/resources/unit.rb +3 -2
- data/lib/fortnox/version.rb +1 -1
- data/lib/fortnox.rb +16 -7
- metadata +2 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 355563e337538c18df2917f19aee3e0d91a1f7763fbb3a01bad441bd3956f496
|
|
4
|
+
data.tar.gz: 4898bf90e631edd152c4c44b1fda68c2284cc2f24e38cd712a8a0057e27a6c40
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 5acaea5d925b3c8f60f817ff39d167f0e078e5ec32f7f86568cf853935ac4955431dd037c81b561ec2be081e09d46b1dbae9b3c3d204bbfbcfd2615bc4d19d50
|
|
7
|
+
data.tar.gz: 14ddc8dbdc2da42f8c09d6bb5ee11c47ceb6fa8faba6cd51da7c97d1fc4bbf4c3adcba67e2bbc831c2783a2841d4f292b3f358d608e31d84cae8e25222fa711b
|
data/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,32 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/)
|
|
|
6
6
|
and this project adheres to
|
|
7
7
|
[Semantic Versioning](http://semver.org/spec/v2.0.0.html).
|
|
8
8
|
|
|
9
|
+
## [1.0.0.rc2] - 2026-05-05
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
|
|
13
|
+
- Per-resource OAuth scope declarations via the `scope` setting in
|
|
14
|
+
`Fortnox::Resource`, plus `Fortnox.scopes` returning a
|
|
15
|
+
`{ scope_string => [resource_classes] }` mapping derived from the
|
|
16
|
+
registered resources.
|
|
17
|
+
|
|
18
|
+
### Changed
|
|
19
|
+
|
|
20
|
+
- `fortnox-setup` lists the OAuth scopes covered by the gem's resources
|
|
21
|
+
and accepts a space-separated selection or `all`, replacing the
|
|
22
|
+
prescriptive default that didn't reflect the actual resource set. Other
|
|
23
|
+
Fortnox scopes (`salary`, `bookkeeping`, etc.) can still be entered
|
|
24
|
+
manually.
|
|
25
|
+
- `TermsOfPayment.code` is now `Sized::String[25]` and required, matching
|
|
26
|
+
Fortnox API documentation. The rest-easy rewrite for rc1 briefly lost
|
|
27
|
+
the required flag.
|
|
28
|
+
- `Unit.code` is now `Sized::String[20]` and required, matching Fortnox
|
|
29
|
+
API documentation. The rest-easy rewrite for rc1 briefly lost the
|
|
30
|
+
required flag.
|
|
31
|
+
- `Unit.description` is now `Sized::String[100]` and required, matching
|
|
32
|
+
Fortnox API documentation. In 0.x and rc1 this was nullable client-side,
|
|
33
|
+
but the Fortnox API rejected unset descriptions anyway.
|
|
34
|
+
|
|
9
35
|
## [1.0.0.rc1] - 2026-05-04
|
|
10
36
|
|
|
11
37
|
Version 1.0 is a complete rewrite of the gem and is **not** a drop-in
|
|
@@ -24,6 +50,14 @@ for the full list of breaking changes.
|
|
|
24
50
|
- **Breaking** Authorization now uses the new Fortnox client credentials flow.
|
|
25
51
|
Refresh tokens are no longer needed, nor supported. A tenant ID is now required;
|
|
26
52
|
obtain one with the new `fortnox-setup` executable.
|
|
53
|
+
- **Breaking** Environment variables lose the `_API_` infix:
|
|
54
|
+
`FORTNOX_API_CLIENT_ID` → `FORTNOX_CLIENT_ID`, `FORTNOX_API_CLIENT_SECRET`
|
|
55
|
+
→ `FORTNOX_CLIENT_SECRET`, `FORTNOX_API_ACCESS_TOKEN` →
|
|
56
|
+
`FORTNOX_ACCESS_TOKEN`. `FORTNOX_API_REFRESH_TOKEN`,
|
|
57
|
+
`FORTNOX_API_REDIRECT_URI`, and `FORTNOX_API_SCOPES` are removed —
|
|
58
|
+
refresh tokens are no longer supported, and the redirect URI and scopes
|
|
59
|
+
are now selected interactively in `fortnox-setup`. The new
|
|
60
|
+
`FORTNOX_TENANT_ID` is required for the client credentials flow.
|
|
27
61
|
- **Breaking** `Fortnox.request_access_token` replaces
|
|
28
62
|
`Fortnox::API::Repository::Authentication` for token management.
|
|
29
63
|
- **Breaking** Configuration moves from `Fortnox::API.configuration` to
|
|
@@ -60,6 +94,16 @@ for the full list of breaking changes.
|
|
|
60
94
|
`size`, `length`, `empty?`, `[]`, and `to_a`, so most existing Array
|
|
61
95
|
usage works unchanged. Code that explicitly checks `is_a?(Array)` or
|
|
62
96
|
compares with `==` against an Array literal needs updating.
|
|
97
|
+
- **Breaking** `Invoice.accounting_method` is now an enum accepting only
|
|
98
|
+
`''`, `'ACCRUAL'`, or `'CASH'`. In 0.x this was a free-form
|
|
99
|
+
`Nullable::String` so any value passed client-side.
|
|
100
|
+
- **Breaking** `Invoice.invoice_type` is now an enum accepting only `''`,
|
|
101
|
+
`'INVOICE'`, `'AGREEMENTINVOICE'`, `'INTRESTINVOICE'`, `'SUMMARYINVOICE'`,
|
|
102
|
+
or `'CASHINVOICE'`. In 0.x this was a free-form `Nullable::String`.
|
|
103
|
+
- `your_order_number` on Invoice and Order (inherited from Document) max
|
|
104
|
+
length raised from 30 to 75 characters to match the current Fortnox API.
|
|
105
|
+
- Article dimension fields (`depth`, `height`, `weight`, `width`) max raised
|
|
106
|
+
from 99,999,999 to 999,999,999 to match the documented Fortnox range.
|
|
63
107
|
|
|
64
108
|
### Added
|
|
65
109
|
|
|
@@ -94,3 +138,6 @@ for the full list of breaking changes.
|
|
|
94
138
|
|
|
95
139
|
For changes prior to the 1.0 rewrite, see the
|
|
96
140
|
[0.x changelog](https://github.com/accodeing/fortnox-api/blob/v0.9.2/CHANGELOG.md).
|
|
141
|
+
|
|
142
|
+
[1.0.0.rc2]: https://github.com/accodeing/fortnox-api/compare/v1.0.0.rc1...v1.0.0.rc2
|
|
143
|
+
[1.0.0.rc1]: https://github.com/accodeing/fortnox-api/releases/tag/v1.0.0.rc1
|
data/README.md
CHANGED
|
@@ -16,7 +16,7 @@ Adding more resources is quick and easy, see the
|
|
|
16
16
|
## Status
|
|
17
17
|
|
|
18
18
|
Version 1.0 is a complete rewrite, currently in release candidate
|
|
19
|
-
(`1.0.0.
|
|
19
|
+
(`1.0.0.rc2`). It is built on
|
|
20
20
|
[rest-easy](https://github.com/accodeing/rest-easy), replacing the old
|
|
21
21
|
HTTParty + Data Mapper architecture with a single resource class per entity.
|
|
22
22
|
Authorization uses the Fortnox client credentials flow.
|
|
@@ -35,21 +35,21 @@ snake_case in Ruby).
|
|
|
35
35
|
|
|
36
36
|
### Immutability
|
|
37
37
|
|
|
38
|
-
|
|
38
|
+
Resource instances are immutable. That means:
|
|
39
39
|
|
|
40
40
|
```ruby
|
|
41
|
-
customer.
|
|
42
|
-
customer.
|
|
41
|
+
customer.name # => "Old Name"
|
|
42
|
+
customer.name = 'New Name' # => NoMethodError
|
|
43
43
|
```
|
|
44
44
|
|
|
45
45
|
Any operation that updates state returns a new instance with the updated
|
|
46
46
|
attributes while leaving the old instance alone:
|
|
47
47
|
|
|
48
48
|
```ruby
|
|
49
|
-
customer.
|
|
49
|
+
customer.name # => "Old Name"
|
|
50
50
|
updated_customer = customer.update(name: 'New Name')
|
|
51
|
-
updated_customer.
|
|
52
|
-
customer.
|
|
51
|
+
updated_customer.name # => "New Name"
|
|
52
|
+
customer.name # => "Old Name"
|
|
53
53
|
```
|
|
54
54
|
|
|
55
55
|
This is how all resources work, they are all immutable.
|
|
@@ -141,20 +141,26 @@ fortnox-setup
|
|
|
141
141
|
|
|
142
142
|
The script will:
|
|
143
143
|
|
|
144
|
-
1. Ask for your client ID
|
|
145
|
-
2.
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
3.
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
144
|
+
1. Ask for your client ID and client secret.
|
|
145
|
+
2. List the OAuth scopes covered by the gem's resources and ask which ones
|
|
146
|
+
you need. Enter a space-separated list, or `all` for everything the gem
|
|
147
|
+
supports. You can also enter scopes the gem doesn't expose directly
|
|
148
|
+
if you plan to call those endpoints manually.
|
|
149
|
+
3. Offer to use a local server to catch the authorization response automatically.
|
|
150
|
+
If you choose this, set your Fortnox app's redirect URL to `http://localhost:4242`.
|
|
151
|
+
Otherwise, enter your existing redirect URL and paste the authorization code manually.
|
|
152
|
+
4. Open your browser to the Fortnox authorization page.
|
|
153
|
+
5. You log in to Fortnox and grant your app access.
|
|
154
|
+
6. The script exchanges the authorization code for an access token and extracts
|
|
155
|
+
the tenant ID from the JWT.
|
|
156
|
+
7. The tenant ID is printed for you to store in your application's configuration.
|
|
154
157
|
|
|
155
158
|
After this you have a tenant ID and never need to run this script again (unless
|
|
156
159
|
you need to authorize against a different Fortnox account).
|
|
157
160
|
|
|
161
|
+
Note: If you change the integration configuration in Fortnox it takes some time for
|
|
162
|
+
Fortnox to propagate the changes (for instance changing the Redirect URI or the scope).
|
|
163
|
+
|
|
158
164
|
### Requesting access tokens
|
|
159
165
|
|
|
160
166
|
Once you have a tenant ID, you can request access tokens programmatically.
|
|
@@ -176,6 +182,11 @@ It is up to you to manage the token lifecycle in your application. A common
|
|
|
176
182
|
approach is to request a new token before each batch of API calls, or to cache
|
|
177
183
|
the token and refresh it when it expires.
|
|
178
184
|
|
|
185
|
+
Note: Fortnox only allows one active access token per integration. Requesting a
|
|
186
|
+
new token invalidates the previous one. If you need multiple active access
|
|
187
|
+
tokens in parallel, you need a separate integration (client ID and secret) for
|
|
188
|
+
each token.
|
|
189
|
+
|
|
179
190
|
### Updating access tokens in env files
|
|
180
191
|
|
|
181
192
|
For development and testing, the gem includes an executable that reads your
|
|
@@ -228,11 +239,11 @@ returns alongside collection responses:
|
|
|
228
239
|
|
|
229
240
|
```ruby
|
|
230
241
|
customers = Fortnox::Customer.all
|
|
231
|
-
customers.first.
|
|
232
|
-
customers.size
|
|
233
|
-
customers.total
|
|
234
|
-
customers.pages
|
|
235
|
-
customers.current_page
|
|
242
|
+
customers.first.name # => "Acme Corp"
|
|
243
|
+
customers.size # => 50
|
|
244
|
+
customers.total # => 327
|
|
245
|
+
customers.pages # => 7
|
|
246
|
+
customers.current_page # => 1
|
|
236
247
|
```
|
|
237
248
|
|
|
238
249
|
`Collection` is `Enumerable`, so `.each`, `.map`, `.select`, `.first`, etc.
|
|
@@ -265,13 +276,13 @@ See the
|
|
|
265
276
|
[Fortnox documentation](https://developer.fortnox.se/general/parameters/)
|
|
266
277
|
for available parameters.
|
|
267
278
|
|
|
268
|
-
|
|
279
|
+
Attributes are exposed directly on the returned instance:
|
|
269
280
|
|
|
270
281
|
```ruby
|
|
271
282
|
customer = Fortnox::Customer.find(1)
|
|
272
|
-
customer.
|
|
273
|
-
customer.
|
|
274
|
-
customer.unique_id
|
|
283
|
+
customer.name # => "Acme Corp"
|
|
284
|
+
customer.city # => "Stockholm"
|
|
285
|
+
customer.unique_id # => "1"
|
|
275
286
|
```
|
|
276
287
|
|
|
277
288
|
### Creating a record
|
|
@@ -281,7 +292,7 @@ Use `.stub` to build a new instance and `.save` to persist it:
|
|
|
281
292
|
```ruby
|
|
282
293
|
customer = Fortnox::Customer.stub(name: 'Acme Corp', city: 'Stockholm')
|
|
283
294
|
result = Fortnox::Customer.save(customer)
|
|
284
|
-
result.
|
|
295
|
+
result.customer_number # => "1"
|
|
285
296
|
```
|
|
286
297
|
|
|
287
298
|
### Updating a record
|
data/bin/fortnox-setup
CHANGED
|
@@ -11,6 +11,7 @@ require 'securerandom'
|
|
|
11
11
|
require 'socket'
|
|
12
12
|
require 'uri'
|
|
13
13
|
require 'faraday'
|
|
14
|
+
require 'fortnox'
|
|
14
15
|
|
|
15
16
|
OAUTH_ENDPOINT = 'https://apps.fortnox.se/oauth-v1'
|
|
16
17
|
LOCAL_PORT = 4242
|
|
@@ -108,10 +109,25 @@ puts
|
|
|
108
109
|
|
|
109
110
|
client_id = prompt('Client ID')
|
|
110
111
|
client_secret = prompt('Client secret')
|
|
112
|
+
|
|
113
|
+
puts
|
|
114
|
+
puts 'Available scopes:'
|
|
115
|
+
Fortnox.scopes.each do |scope, resources|
|
|
116
|
+
resource_names = resources.map { |r| r.name.split('::').last }.join(', ')
|
|
117
|
+
puts " #{scope.ljust(10)} → #{resource_names}"
|
|
118
|
+
end
|
|
119
|
+
puts
|
|
120
|
+
puts 'These must match the scopes configured on your Fortnox app in the developer'
|
|
121
|
+
puts 'portal. Other Fortnox scopes (salary, bookkeeping, etc.) can be added manually.'
|
|
111
122
|
puts
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
123
|
+
|
|
124
|
+
scopes = loop do
|
|
125
|
+
input = prompt("Enter scopes (space-separated, or 'all')").strip
|
|
126
|
+
break Fortnox.scopes.keys.join(' ') if input == 'all'
|
|
127
|
+
break input unless input.empty?
|
|
128
|
+
|
|
129
|
+
puts 'Please enter at least one scope.'
|
|
130
|
+
end
|
|
115
131
|
|
|
116
132
|
puts
|
|
117
133
|
puts 'The script can catch the authorization response automatically using a'
|
data/lib/fortnox/resource.rb
CHANGED
|
@@ -7,8 +7,11 @@ module Fortnox
|
|
|
7
7
|
settings do
|
|
8
8
|
setting :instance_wrapper, reader: true
|
|
9
9
|
setting :collection_wrapper, reader: true
|
|
10
|
+
setting :scope, reader: true
|
|
10
11
|
end
|
|
11
12
|
|
|
13
|
+
@registered_resources = []
|
|
14
|
+
|
|
12
15
|
before_parse do |data, meta|
|
|
13
16
|
if data.key?(config.instance_wrapper)
|
|
14
17
|
meta.partial = false
|
|
@@ -38,6 +41,13 @@ module Fortnox
|
|
|
38
41
|
end
|
|
39
42
|
|
|
40
43
|
class << self
|
|
44
|
+
attr_reader :registered_resources
|
|
45
|
+
|
|
46
|
+
def inherited(subclass)
|
|
47
|
+
super
|
|
48
|
+
Fortnox::Resource.registered_resources << subclass
|
|
49
|
+
end
|
|
50
|
+
|
|
41
51
|
def parse(response)
|
|
42
52
|
with_translated_errors do
|
|
43
53
|
pagination = extract_pagination(response)
|
|
@@ -8,13 +8,14 @@ module Fortnox
|
|
|
8
8
|
path 'termsofpayments'
|
|
9
9
|
instance_wrapper 'TermsOfPayment'
|
|
10
10
|
collection_wrapper 'TermsOfPayments'
|
|
11
|
+
scope 'settings'
|
|
11
12
|
end
|
|
12
13
|
|
|
13
14
|
# @url Direct URL to the record.
|
|
14
15
|
attr :url <=> '@url', Coercible::String.optional, :read_only
|
|
15
16
|
|
|
16
17
|
# Code The code of the term of payment.
|
|
17
|
-
key :code,
|
|
18
|
+
key :code, Sized::String[25], :required
|
|
18
19
|
|
|
19
20
|
# Description The description of the term of payment.
|
|
20
21
|
attr :description, Strict::String, :required
|
|
@@ -8,16 +8,17 @@ module Fortnox
|
|
|
8
8
|
path 'units'
|
|
9
9
|
instance_wrapper 'Unit'
|
|
10
10
|
collection_wrapper 'Units'
|
|
11
|
+
scope 'settings'
|
|
11
12
|
end
|
|
12
13
|
|
|
13
14
|
# @url Direct URL to the record.
|
|
14
15
|
attr :url <=> '@url', Coercible::String.optional, :read_only
|
|
15
16
|
|
|
16
17
|
# Code The code of the unit.
|
|
17
|
-
key :code,
|
|
18
|
+
key :code, Sized::String[20], :required
|
|
18
19
|
|
|
19
20
|
# Description The description of the unit.
|
|
20
|
-
attr :description,
|
|
21
|
+
attr :description, Sized::String[100], :required
|
|
21
22
|
|
|
22
23
|
# CodeEnglish English code of the unit
|
|
23
24
|
attr :code_english, Sized::String[100]
|
data/lib/fortnox/version.rb
CHANGED
data/lib/fortnox.rb
CHANGED
|
@@ -5,14 +5,14 @@ require 'json'
|
|
|
5
5
|
require 'rest_easy'
|
|
6
6
|
require 'zeitwerk'
|
|
7
7
|
|
|
8
|
-
loader = Zeitwerk::Loader.for_gem
|
|
9
|
-
loader.collapse("#{__dir__}/fortnox/resources")
|
|
10
|
-
loader.inflector.inflect(
|
|
11
|
-
'edi_information' => 'EDIInformation'
|
|
12
|
-
)
|
|
13
|
-
loader.setup
|
|
14
|
-
|
|
15
8
|
module Fortnox
|
|
9
|
+
@loader = Zeitwerk::Loader.for_gem
|
|
10
|
+
@loader.collapse("#{__dir__}/fortnox/resources")
|
|
11
|
+
@loader.inflector.inflect(
|
|
12
|
+
'edi_information' => 'EDIInformation'
|
|
13
|
+
)
|
|
14
|
+
@loader.setup
|
|
15
|
+
|
|
16
16
|
extend RestEasy
|
|
17
17
|
|
|
18
18
|
class Error < StandardError; end
|
|
@@ -76,6 +76,15 @@ module Fortnox
|
|
|
76
76
|
parsed['access_token']
|
|
77
77
|
end
|
|
78
78
|
|
|
79
|
+
def scopes
|
|
80
|
+
@loader.eager_load
|
|
81
|
+
Resource.registered_resources
|
|
82
|
+
.group_by(&:scope)
|
|
83
|
+
.reject { |scope, _| scope.nil? }
|
|
84
|
+
.sort
|
|
85
|
+
.to_h
|
|
86
|
+
end
|
|
87
|
+
|
|
79
88
|
private
|
|
80
89
|
|
|
81
90
|
def token_request(client_id, client_secret, tenant_id, scopes)
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: fortnox-api
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 1.0.0.
|
|
4
|
+
version: 1.0.0.rc2
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Jonas Schubert Erlandsson
|
|
@@ -11,7 +11,7 @@ authors:
|
|
|
11
11
|
autorequire:
|
|
12
12
|
bindir: bin
|
|
13
13
|
cert_chain: []
|
|
14
|
-
date: 2026-05-
|
|
14
|
+
date: 2026-05-05 00:00:00.000000000 Z
|
|
15
15
|
dependencies:
|
|
16
16
|
- !ruby/object:Gem::Dependency
|
|
17
17
|
name: countries
|