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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: d4ab3f55b3783c477974abba27bae6b9104baf418e131532a746f822177605bd
4
- data.tar.gz: 882d8f34d8246f74052823894fd7286a146bdaa745155928059cad7abfe0d6ef
3
+ metadata.gz: 355563e337538c18df2917f19aee3e0d91a1f7763fbb3a01bad441bd3956f496
4
+ data.tar.gz: 4898bf90e631edd152c4c44b1fda68c2284cc2f24e38cd712a8a0057e27a6c40
5
5
  SHA512:
6
- metadata.gz: 2e4a87ea6c15df6acce08ef2e3006f6cf4acd936a933e8449af585f5426f0a5f951e06f84a96ec34a044248519b013d2085b3c7ff8889e5016a4cc79af0074f1
7
- data.tar.gz: '09c2e017f7dc98c61565c0dc00b9d8ab35a8ea8ad90928e523f822c5f6bb84a2ebc74c75cbd1d02a4cb2ba8d58147f39286b0de05c656170a7f55ae6f4792971'
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.rc1`). It is built on
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
- The model instances are immutable. That means:
38
+ Resource instances are immutable. That means:
39
39
 
40
40
  ```ruby
41
- customer.model.name # => "Old Name"
42
- customer.model.name = 'New Name' # => NoMethodError
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.model.name # => "Old Name"
49
+ customer.name # => "Old Name"
50
50
  updated_customer = customer.update(name: 'New Name')
51
- updated_customer.model.name # => "New Name"
52
- customer.model.name # => "Old Name"
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, client secret, and scopes
145
- 2. Offer to use a local server on `http://localhost:4242` to catch the
146
- authorization response automatically. If you choose this, set your Fortnox
147
- app's redirect URL to `http://localhost:4242`. Otherwise, enter your existing
148
- redirect URL and paste the authorization code manually.
149
- 3. Open your browser to the Fortnox authorization page
150
- 4. You log in to Fortnox and grant your app access
151
- 5. The script exchanges the authorization code for an access token and extracts
152
- the tenant ID from the JWT
153
- 6. The tenant ID is printed for you to store in your application's configuration
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.model.name # => "Acme Corp"
232
- customers.size # => 50
233
- customers.total # => 327
234
- customers.pages # => 7
235
- customers.current_page # => 1
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
- The returned object wraps the model. Access attributes through `.model`:
279
+ Attributes are exposed directly on the returned instance:
269
280
 
270
281
  ```ruby
271
282
  customer = Fortnox::Customer.find(1)
272
- customer.model.name # => "Acme Corp"
273
- customer.model.city # => "Stockholm"
274
- customer.unique_id # => "1"
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.model.customer_number # => "1"
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
- puts 'Enter the scopes you need. These must match the scopes configured on your'
113
- puts 'Fortnox app in the developer portal.'
114
- scopes = prompt('Scopes', default: 'article customer invoice order project settings')
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'
@@ -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,6 +8,7 @@ module Fortnox
8
8
  path 'articles'
9
9
  instance_wrapper 'Article'
10
10
  collection_wrapper 'Articles'
11
+ scope 'article'
11
12
  end
12
13
 
13
14
  # @url Direct URL to the record.
@@ -8,6 +8,7 @@ module Fortnox
8
8
  path 'customers'
9
9
  instance_wrapper 'Customer'
10
10
  collection_wrapper 'Customers'
11
+ scope 'customer'
11
12
  end
12
13
 
13
14
  # Direct URL to the record.
@@ -8,6 +8,7 @@ module Fortnox
8
8
  path 'invoices'
9
9
  instance_wrapper 'Invoice'
10
10
  collection_wrapper 'Invoices'
11
+ scope 'invoice'
11
12
  end
12
13
 
13
14
  # AccountingMethod Accounting Method.
@@ -6,6 +6,7 @@ module Fortnox
6
6
  path 'labels'
7
7
  instance_wrapper 'Label'
8
8
  collection_wrapper 'Labels'
9
+ scope 'settings'
9
10
  end
10
11
 
11
12
  attr :id, Coercible::Integer.optional, :read_only, :key
@@ -8,6 +8,7 @@ module Fortnox
8
8
  path 'orders'
9
9
  instance_wrapper 'Order'
10
10
  collection_wrapper 'Orders'
11
+ scope 'order'
11
12
  end
12
13
 
13
14
  # CopyRemarks If remarks shall be copied from order to invoice
@@ -8,6 +8,7 @@ module Fortnox
8
8
  path 'projects'
9
9
  instance_wrapper 'Project'
10
10
  collection_wrapper 'Projects'
11
+ scope 'project'
11
12
  end
12
13
 
13
14
  # @url Direct URL to the record.
@@ -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, Strict::String
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, Strict::String
18
+ key :code, Sized::String[20], :required
18
19
 
19
20
  # Description The description of the unit.
20
- attr :description, Coercible::String.optional
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]
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Fortnox
4
- VERSION = '1.0.0.rc1'
4
+ VERSION = '1.0.0.rc2'
5
5
  end
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.rc1
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-04 00:00:00.000000000 Z
14
+ date: 2026-05-05 00:00:00.000000000 Z
15
15
  dependencies:
16
16
  - !ruby/object:Gem::Dependency
17
17
  name: countries