fortnox-api 1.0.0.rc1 → 1.0.0.rc3
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 +77 -0
- data/README.md +42 -29
- data/bin/fortnox-setup +19 -3
- data/lib/fortnox/mappers/document_row.rb +2 -1
- data/lib/fortnox/resource.rb +10 -0
- data/lib/fortnox/resources/article.rb +2 -1
- data/lib/fortnox/resources/customer.rb +4 -3
- data/lib/fortnox/resources/document.rb +3 -3
- data/lib/fortnox/resources/invoice.rb +4 -3
- 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 +55 -8
- 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: 2297de9b35ce0c8d7f3f0e3b84f0a44a443062d257f843876657a6d26a4da160
|
|
4
|
+
data.tar.gz: b328a297acaa35278c2b943b10bc2dc04b7e8dfbf2dd65642346f35c572bb950
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: a73b124f68428feaaccbfea406a025bb24995b7dce7e88959dee7aca21ab449e4c92cbae11808b33c50cf8f8e498d6039399157cdcc5e68c3bcb6cfce5a35b30
|
|
7
|
+
data.tar.gz: aa5b472af7edc87d4f808581ae99545e1b99f33d7319890843d442d9f2c7d1afe7db6fb04d509a5524d5b7f405337e0b6043d8155be1e5b3ebf2b8e83d3e75e9
|
data/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,61 @@ 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.rc3] - 2026-05-08
|
|
10
|
+
|
|
11
|
+
### Changed
|
|
12
|
+
|
|
13
|
+
- `Fortnox::RequestError#message` now includes the API's
|
|
14
|
+
`ErrorInformation.Message` and code from the response body when present,
|
|
15
|
+
normalising Fortnox's inconsistent key casing (PascalCase vs lowercase)
|
|
16
|
+
across endpoints. Previously the message was only `"Request failed: <status>"`,
|
|
17
|
+
hiding the cause from logs and uncaught backtraces.
|
|
18
|
+
- `Article#commodity_code`, `Customer#phone`, `Invoice#accounting_method`,
|
|
19
|
+
`Invoice#invoice_period_reference`, `Invoice#invoice_reference`,
|
|
20
|
+
`Document#time_basis_reference`, `Document#total_to_pay`, and
|
|
21
|
+
`Document#warehouse_ready` are now flagged read-only to match the Fortnox
|
|
22
|
+
API. Values set on these attributes are silently excluded from save
|
|
23
|
+
requests; previously they were sent and rejected by the API.
|
|
24
|
+
|
|
25
|
+
### Fixed
|
|
26
|
+
|
|
27
|
+
- `Order` and `Invoice` rows now serialise the VAT field as `VAT` instead of
|
|
28
|
+
`Vat`. Saving rows with a `vat` value previously failed with Fortnox
|
|
29
|
+
rejecting the request as `"Felaktigt fältnamn"`. This bug was introduced in the 1.0.0.rc1,
|
|
30
|
+
it did not exist in 0.x.
|
|
31
|
+
|
|
32
|
+
## [1.0.0.rc2] - 2026-05-05
|
|
33
|
+
|
|
34
|
+
### Added
|
|
35
|
+
|
|
36
|
+
- Per-resource OAuth scope declarations via the `scope` setting in
|
|
37
|
+
`Fortnox::Resource`, plus `Fortnox.scopes` returning a
|
|
38
|
+
`{ scope_string => [resource_classes] }` mapping derived from the
|
|
39
|
+
registered resources.
|
|
40
|
+
|
|
41
|
+
### Changed
|
|
42
|
+
|
|
43
|
+
- `fortnox-setup` lists the OAuth scopes covered by the gem's resources
|
|
44
|
+
and accepts a space-separated selection or `all`, replacing the
|
|
45
|
+
prescriptive default that didn't reflect the actual resource set. Other
|
|
46
|
+
Fortnox scopes (`salary`, `bookkeeping`, etc.) can still be entered
|
|
47
|
+
manually.
|
|
48
|
+
- `TermsOfPayment.code` now has a 25-character limit, matching Fortnox
|
|
49
|
+
API documentation.
|
|
50
|
+
- `Unit.code` now has a 20-character limit, matching Fortnox API
|
|
51
|
+
documentation.
|
|
52
|
+
- `Unit.description` is now `Sized::String[100]` and required, matching
|
|
53
|
+
Fortnox API documentation. In 0.x and rc1 this was nullable client-side,
|
|
54
|
+
but the Fortnox API rejected unset descriptions anyway.
|
|
55
|
+
|
|
56
|
+
### Fixed
|
|
57
|
+
|
|
58
|
+
- `TermsOfPayment.code` is required again, matching Fortnox API
|
|
59
|
+
documentation. The rest-easy rewrite for rc1 briefly lost the required
|
|
60
|
+
flag.
|
|
61
|
+
- `Unit.code` is required again, matching Fortnox API documentation. The
|
|
62
|
+
rest-easy rewrite for rc1 briefly lost the required flag.
|
|
63
|
+
|
|
9
64
|
## [1.0.0.rc1] - 2026-05-04
|
|
10
65
|
|
|
11
66
|
Version 1.0 is a complete rewrite of the gem and is **not** a drop-in
|
|
@@ -24,6 +79,14 @@ for the full list of breaking changes.
|
|
|
24
79
|
- **Breaking** Authorization now uses the new Fortnox client credentials flow.
|
|
25
80
|
Refresh tokens are no longer needed, nor supported. A tenant ID is now required;
|
|
26
81
|
obtain one with the new `fortnox-setup` executable.
|
|
82
|
+
- **Breaking** Environment variables lose the `_API_` infix:
|
|
83
|
+
`FORTNOX_API_CLIENT_ID` → `FORTNOX_CLIENT_ID`, `FORTNOX_API_CLIENT_SECRET`
|
|
84
|
+
→ `FORTNOX_CLIENT_SECRET`, `FORTNOX_API_ACCESS_TOKEN` →
|
|
85
|
+
`FORTNOX_ACCESS_TOKEN`. `FORTNOX_API_REFRESH_TOKEN`,
|
|
86
|
+
`FORTNOX_API_REDIRECT_URI`, and `FORTNOX_API_SCOPES` are removed —
|
|
87
|
+
refresh tokens are no longer supported, and the redirect URI and scopes
|
|
88
|
+
are now selected interactively in `fortnox-setup`. The new
|
|
89
|
+
`FORTNOX_TENANT_ID` is required for the client credentials flow.
|
|
27
90
|
- **Breaking** `Fortnox.request_access_token` replaces
|
|
28
91
|
`Fortnox::API::Repository::Authentication` for token management.
|
|
29
92
|
- **Breaking** Configuration moves from `Fortnox::API.configuration` to
|
|
@@ -60,6 +123,16 @@ for the full list of breaking changes.
|
|
|
60
123
|
`size`, `length`, `empty?`, `[]`, and `to_a`, so most existing Array
|
|
61
124
|
usage works unchanged. Code that explicitly checks `is_a?(Array)` or
|
|
62
125
|
compares with `==` against an Array literal needs updating.
|
|
126
|
+
- **Breaking** `Invoice.accounting_method` is now an enum accepting only
|
|
127
|
+
`''`, `'ACCRUAL'`, or `'CASH'`. In 0.x this was a free-form
|
|
128
|
+
`Nullable::String` so any value passed client-side.
|
|
129
|
+
- **Breaking** `Invoice.invoice_type` is now an enum accepting only `''`,
|
|
130
|
+
`'INVOICE'`, `'AGREEMENTINVOICE'`, `'INTRESTINVOICE'`, `'SUMMARYINVOICE'`,
|
|
131
|
+
or `'CASHINVOICE'`. In 0.x this was a free-form `Nullable::String`.
|
|
132
|
+
- `your_order_number` on Invoice and Order (inherited from Document) max
|
|
133
|
+
length raised from 30 to 75 characters to match the current Fortnox API.
|
|
134
|
+
- Article dimension fields (`depth`, `height`, `weight`, `width`) max raised
|
|
135
|
+
from 99,999,999 to 999,999,999 to match the documented Fortnox range.
|
|
63
136
|
|
|
64
137
|
### Added
|
|
65
138
|
|
|
@@ -94,3 +167,7 @@ for the full list of breaking changes.
|
|
|
94
167
|
|
|
95
168
|
For changes prior to the 1.0 rewrite, see the
|
|
96
169
|
[0.x changelog](https://github.com/accodeing/fortnox-api/blob/v0.9.2/CHANGELOG.md).
|
|
170
|
+
|
|
171
|
+
[1.0.0.rc3]: https://github.com/accodeing/fortnox-api/compare/v1.0.0.rc2...v1.0.0.rc3
|
|
172
|
+
[1.0.0.rc2]: https://github.com/accodeing/fortnox-api/compare/v1.0.0.rc1...v1.0.0.rc2
|
|
173
|
+
[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.rc3`). 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.
|
|
@@ -68,8 +68,10 @@ The gem raises the following exceptions:
|
|
|
68
68
|
|
|
69
69
|
- `Fortnox::Error` — base class for everything below. Rescue this to catch
|
|
70
70
|
any error raised by the gem.
|
|
71
|
-
- `Fortnox::RequestError` — 4xx/5xx responses from the Fortnox API.
|
|
72
|
-
the
|
|
71
|
+
- `Fortnox::RequestError` — 4xx/5xx responses from the Fortnox API. The
|
|
72
|
+
exception message includes the API's `ErrorInformation.Message` and code
|
|
73
|
+
when present (Fortnox is inconsistent about the key casing — the gem
|
|
74
|
+
normalises both). The full response is on `.response`.
|
|
73
75
|
- `Fortnox::AttributeError` — base for attribute validation failures.
|
|
74
76
|
- `Fortnox::ConstraintError` — an attribute value violates a type
|
|
75
77
|
constraint (max size, format, etc.). Carries `.attribute_name` and
|
|
@@ -141,20 +143,26 @@ fortnox-setup
|
|
|
141
143
|
|
|
142
144
|
The script will:
|
|
143
145
|
|
|
144
|
-
1. Ask for your client ID
|
|
145
|
-
2.
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
3.
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
146
|
+
1. Ask for your client ID and client secret.
|
|
147
|
+
2. List the OAuth scopes covered by the gem's resources and ask which ones
|
|
148
|
+
you need. Enter a space-separated list, or `all` for everything the gem
|
|
149
|
+
supports. You can also enter scopes the gem doesn't expose directly
|
|
150
|
+
if you plan to call those endpoints manually.
|
|
151
|
+
3. Offer to use a local server to catch the authorization response automatically.
|
|
152
|
+
If you choose this, set your Fortnox app's redirect URL to `http://localhost:4242`.
|
|
153
|
+
Otherwise, enter your existing redirect URL and paste the authorization code manually.
|
|
154
|
+
4. Open your browser to the Fortnox authorization page.
|
|
155
|
+
5. You log in to Fortnox and grant your app access.
|
|
156
|
+
6. The script exchanges the authorization code for an access token and extracts
|
|
157
|
+
the tenant ID from the JWT.
|
|
158
|
+
7. The tenant ID is printed for you to store in your application's configuration.
|
|
154
159
|
|
|
155
160
|
After this you have a tenant ID and never need to run this script again (unless
|
|
156
161
|
you need to authorize against a different Fortnox account).
|
|
157
162
|
|
|
163
|
+
Note: If you change the integration configuration in Fortnox it takes some time for
|
|
164
|
+
Fortnox to propagate the changes (for instance changing the Redirect URI or the scope).
|
|
165
|
+
|
|
158
166
|
### Requesting access tokens
|
|
159
167
|
|
|
160
168
|
Once you have a tenant ID, you can request access tokens programmatically.
|
|
@@ -176,6 +184,11 @@ It is up to you to manage the token lifecycle in your application. A common
|
|
|
176
184
|
approach is to request a new token before each batch of API calls, or to cache
|
|
177
185
|
the token and refresh it when it expires.
|
|
178
186
|
|
|
187
|
+
Note: Fortnox only allows one active access token per integration. Requesting a
|
|
188
|
+
new token invalidates the previous one. If you need multiple active access
|
|
189
|
+
tokens in parallel, you need a separate integration (client ID and secret) for
|
|
190
|
+
each token.
|
|
191
|
+
|
|
179
192
|
### Updating access tokens in env files
|
|
180
193
|
|
|
181
194
|
For development and testing, the gem includes an executable that reads your
|
|
@@ -228,11 +241,11 @@ returns alongside collection responses:
|
|
|
228
241
|
|
|
229
242
|
```ruby
|
|
230
243
|
customers = Fortnox::Customer.all
|
|
231
|
-
customers.first.
|
|
232
|
-
customers.size
|
|
233
|
-
customers.total
|
|
234
|
-
customers.pages
|
|
235
|
-
customers.current_page
|
|
244
|
+
customers.first.name # => "Acme Corp"
|
|
245
|
+
customers.size # => 50
|
|
246
|
+
customers.total # => 327
|
|
247
|
+
customers.pages # => 7
|
|
248
|
+
customers.current_page # => 1
|
|
236
249
|
```
|
|
237
250
|
|
|
238
251
|
`Collection` is `Enumerable`, so `.each`, `.map`, `.select`, `.first`, etc.
|
|
@@ -265,13 +278,13 @@ See the
|
|
|
265
278
|
[Fortnox documentation](https://developer.fortnox.se/general/parameters/)
|
|
266
279
|
for available parameters.
|
|
267
280
|
|
|
268
|
-
|
|
281
|
+
Attributes are exposed directly on the returned instance:
|
|
269
282
|
|
|
270
283
|
```ruby
|
|
271
284
|
customer = Fortnox::Customer.find(1)
|
|
272
|
-
customer.
|
|
273
|
-
customer.
|
|
274
|
-
customer.unique_id
|
|
285
|
+
customer.name # => "Acme Corp"
|
|
286
|
+
customer.city # => "Stockholm"
|
|
287
|
+
customer.unique_id # => "1"
|
|
275
288
|
```
|
|
276
289
|
|
|
277
290
|
### Creating a record
|
|
@@ -281,7 +294,7 @@ Use `.stub` to build a new instance and `.save` to persist it:
|
|
|
281
294
|
```ruby
|
|
282
295
|
customer = Fortnox::Customer.stub(name: 'Acme Corp', city: 'Stockholm')
|
|
283
296
|
result = Fortnox::Customer.save(customer)
|
|
284
|
-
result.
|
|
297
|
+
result.customer_number # => "1"
|
|
285
298
|
```
|
|
286
299
|
|
|
287
300
|
### 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,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.
|
|
@@ -154,6 +155,6 @@ module Fortnox
|
|
|
154
155
|
attr :default_stock_location, Coercible::String.optional
|
|
155
156
|
|
|
156
157
|
# CommodityCode Commodity code of the article.
|
|
157
|
-
attr :commodity_code, Coercible::String.optional
|
|
158
|
+
attr :commodity_code, Coercible::String.optional, :read_only
|
|
158
159
|
end
|
|
159
160
|
end
|
|
@@ -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.
|
|
@@ -130,6 +131,9 @@ module Fortnox
|
|
|
130
131
|
# Our reference
|
|
131
132
|
attr :our_reference, Sized::String[50]
|
|
132
133
|
|
|
134
|
+
# Phone number of the customer. Only present in collection responses.
|
|
135
|
+
attr :phone, Coercible::String.optional, :read_only
|
|
136
|
+
|
|
133
137
|
# First phone number of the customer
|
|
134
138
|
attr :phone1, Sized::String[1024]
|
|
135
139
|
|
|
@@ -190,9 +194,6 @@ module Fortnox
|
|
|
190
194
|
# Active If the customer is active.
|
|
191
195
|
attr :active, Bool.optional, Boolean
|
|
192
196
|
|
|
193
|
-
# Phone number of the customer. Only present in collection responses.
|
|
194
|
-
attr :phone, Coercible::String.optional
|
|
195
|
-
|
|
196
197
|
# External reference
|
|
197
198
|
attr :external_reference, Sized::String[1024]
|
|
198
199
|
|
|
@@ -191,12 +191,12 @@ module Fortnox
|
|
|
191
191
|
attr :outbound_date, Date.optional, Mappers::Date
|
|
192
192
|
|
|
193
193
|
# TimeBasisReference Reference to time basis.
|
|
194
|
-
attr :time_basis_reference, Coercible::Integer.optional
|
|
194
|
+
attr :time_basis_reference, Coercible::Integer.optional, :read_only
|
|
195
195
|
|
|
196
196
|
# TotalToPay Total amount to pay.
|
|
197
|
-
attr :total_to_pay, Coercible::Float.optional
|
|
197
|
+
attr :total_to_pay, Coercible::Float.optional, :read_only
|
|
198
198
|
|
|
199
199
|
# WarehouseReady If the document is warehouse ready.
|
|
200
|
-
attr :warehouse_ready, Bool.optional, Boolean
|
|
200
|
+
attr :warehouse_ready, Bool.optional, :read_only, Boolean
|
|
201
201
|
end
|
|
202
202
|
end
|
|
@@ -8,10 +8,11 @@ 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.
|
|
14
|
-
attr :accounting_method, AccountingMethods
|
|
15
|
+
attr :accounting_method, AccountingMethods, :read_only
|
|
15
16
|
|
|
16
17
|
# Balance Balance of the invoice.
|
|
17
18
|
attr :balance, Coercible::Float.optional, :read_only
|
|
@@ -50,10 +51,10 @@ module Fortnox
|
|
|
50
51
|
attr :invoice_period_end, Date.optional, :read_only, Mappers::Date
|
|
51
52
|
|
|
52
53
|
# InvoicePeriodReference Reference to the invoice period.
|
|
53
|
-
attr :invoice_period_reference, Coercible::String.optional
|
|
54
|
+
attr :invoice_period_reference, Coercible::String.optional, :read_only
|
|
54
55
|
|
|
55
56
|
# InvoiceReference Reference to another invoice.
|
|
56
|
-
attr :invoice_reference, Coercible::String.optional
|
|
57
|
+
attr :invoice_reference, Coercible::String.optional, :read_only
|
|
57
58
|
|
|
58
59
|
# InvoiceRows Separate object
|
|
59
60
|
attr :invoice_rows, Strict::Array.of(Structs::InvoiceRow), Mappers::StructArray.for(Mappers::InvoiceRow)
|
|
@@ -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
|
|
@@ -23,11 +23,49 @@ module Fortnox
|
|
|
23
23
|
def initialize(arg = nil)
|
|
24
24
|
if arg.respond_to?(:status)
|
|
25
25
|
@response = arg
|
|
26
|
-
super("Request failed: #{arg.status}")
|
|
26
|
+
super("Request failed: #{arg.status}#{format_body(arg.body)}")
|
|
27
27
|
else
|
|
28
28
|
super
|
|
29
29
|
end
|
|
30
30
|
end
|
|
31
|
+
|
|
32
|
+
BODY_FALLBACK_LIMIT = 500
|
|
33
|
+
private_constant :BODY_FALLBACK_LIMIT
|
|
34
|
+
|
|
35
|
+
private
|
|
36
|
+
|
|
37
|
+
def format_body(body)
|
|
38
|
+
return '' if body.nil? || body.empty?
|
|
39
|
+
|
|
40
|
+
" - #{error_details(body) || truncate(body.to_s)}"
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# Fortnox have at least in the past sometimes returned HTML responses on error,
|
|
44
|
+
# for instance 503 Service Temporarily Unavailable.
|
|
45
|
+
# In that case, the body might be long and useful for debugging,
|
|
46
|
+
# so let's truncate it to a reasonable length if we end up here.
|
|
47
|
+
def truncate(string)
|
|
48
|
+
string.length > BODY_FALLBACK_LIMIT ? "#{string[0, BODY_FALLBACK_LIMIT]}…" : string
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def error_details(body)
|
|
52
|
+
info = error_information(body)
|
|
53
|
+
message = info && info['message']
|
|
54
|
+
return nil unless message
|
|
55
|
+
|
|
56
|
+
code = info['code']
|
|
57
|
+
code ? "#{message} (#{code})" : message
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def error_information(body)
|
|
61
|
+
parsed = body.is_a?(String) ? JSON.parse(body) : body
|
|
62
|
+
info = parsed['ErrorInformation'] if parsed.is_a?(Hash)
|
|
63
|
+
# Fortnox responds with inconsistently-cased error keys (see tests),
|
|
64
|
+
# so let's normalise to lowercase before reading.
|
|
65
|
+
info.is_a?(Hash) ? info.transform_keys(&:downcase) : nil
|
|
66
|
+
rescue JSON::ParserError
|
|
67
|
+
nil
|
|
68
|
+
end
|
|
31
69
|
end
|
|
32
70
|
|
|
33
71
|
class AttributeError < Error; end
|
|
@@ -76,6 +114,15 @@ module Fortnox
|
|
|
76
114
|
parsed['access_token']
|
|
77
115
|
end
|
|
78
116
|
|
|
117
|
+
def scopes
|
|
118
|
+
@loader.eager_load
|
|
119
|
+
Resource.registered_resources
|
|
120
|
+
.group_by(&:scope)
|
|
121
|
+
.reject { |scope, _| scope.nil? }
|
|
122
|
+
.sort
|
|
123
|
+
.to_h
|
|
124
|
+
end
|
|
125
|
+
|
|
79
126
|
private
|
|
80
127
|
|
|
81
128
|
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.rc3
|
|
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-08 00:00:00.000000000 Z
|
|
15
15
|
dependencies:
|
|
16
16
|
- !ruby/object:Gem::Dependency
|
|
17
17
|
name: countries
|