fortnox-api 1.0.0.rc16 → 1.0.0
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 +60 -1
- data/MIGRATING_TO_1.0.md +518 -0
- data/README.md +1 -2
- data/fortnox.gemspec +10 -2
- data/lib/fortnox/resource.rb +1 -1
- data/lib/fortnox/version.rb +1 -1
- metadata +12 -5
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 5cb997af480b5faeb41221e86f2e2e582509680e3bd9dd37260c4de1efc5d63b
|
|
4
|
+
data.tar.gz: dcd0a9d4965d8f6a810b8ad8d646c104b5c6955ef18a61e587f70497c2cafd69
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 63600c1ae19e3104aea10be4064e1507f55de6365f884c7a9fac0d72beecd2c1d0399676a38bf93fe0c91f305282c2081022bc417576d8c7b809755cbd475445
|
|
7
|
+
data.tar.gz: e8958e7c8b99ea86ad1e7391db05bcd395faabfca787d55f34d391407751dded8b6ad982cc08ff17c2c11babc89aeb63fbfee747a0c8fb95577805c0c458905a
|
data/CHANGELOG.md
CHANGED
|
@@ -8,6 +8,64 @@ and this project adheres to
|
|
|
8
8
|
|
|
9
9
|
## [Unreleased]
|
|
10
10
|
|
|
11
|
+
## [1.0.0] - 2026-09-23
|
|
12
|
+
|
|
13
|
+
The first stable release of the 1.0 line. No code changes since
|
|
14
|
+
`1.0.0.rc16` — the release candidate series is over and the 1.0 API is
|
|
15
|
+
now settled. Consumers pinned to `1.0.0.rc16` can move to `1.0.0` as is;
|
|
16
|
+
from an earlier release candidate, see the entries below for what
|
|
17
|
+
changed in between.
|
|
18
|
+
|
|
19
|
+
### Breaking changes
|
|
20
|
+
|
|
21
|
+
1.0 is a complete rewrite and is **not** a drop-in replacement for 0.x.
|
|
22
|
+
[MIGRATING_TO_1.0.md](MIGRATING_TO_1.0.md) is the guided upgrade; this is
|
|
23
|
+
the checklist to read it against. Each entry names the release candidate
|
|
24
|
+
that introduced it, where the full reasoning and the exact before/after
|
|
25
|
+
live.
|
|
26
|
+
|
|
27
|
+
- **The gem is rebuilt on [rest-easy](https://github.com/accodeing/rest-easy)**,
|
|
28
|
+
one resource class per entity in place of HTTParty + Data Mapper, and the
|
|
29
|
+
namespace moves from `Fortnox::API` to `Fortnox` — `Fortnox::API::Repository::Customer`
|
|
30
|
+
is now `Fortnox::Customer`. (rc1)
|
|
31
|
+
- **Ruby 3.2 or later.** rc1 raised the floor to 3.1 and rc7 to 3.2; coming
|
|
32
|
+
from 0.x, 3.2 is the only number that matters. (rc1, rc7)
|
|
33
|
+
- **Authorization is the Fortnox client credentials flow.** Refresh tokens
|
|
34
|
+
are neither needed nor supported, and a tenant ID is now required — the
|
|
35
|
+
new `fortnox-setup` executable obtains one. (rc1)
|
|
36
|
+
- **Environment variables lose the `_API_` infix** (`FORTNOX_API_CLIENT_ID`
|
|
37
|
+
→ `FORTNOX_CLIENT_ID`, and so on). `FORTNOX_API_REFRESH_TOKEN`,
|
|
38
|
+
`FORTNOX_API_REDIRECT_URI` and `FORTNOX_API_SCOPES` are gone;
|
|
39
|
+
`FORTNOX_TENANT_ID` is new and required. (rc1)
|
|
40
|
+
- **`Fortnox.request_access_token` replaces
|
|
41
|
+
`Fortnox::API::Repository::Authentication`** for token management. (rc1)
|
|
42
|
+
- **Configuration moves to module-level setters** such as
|
|
43
|
+
`Fortnox.access_token=`, from `Fortnox::API.configuration`. (rc1)
|
|
44
|
+
- **Country attributes accept ISO alpha-2 codes only** (`'NO'`, not
|
|
45
|
+
`'Norge'`), on `country_code` and `delivery_country`. (rc1)
|
|
46
|
+
- **Exception classes are renamed and consolidated**, `Fortnox::API::Exception`
|
|
47
|
+
→ `Fortnox::Error` at the root. `Fortnox::API::MissingConfiguration` is
|
|
48
|
+
removed, `Fortnox::ConstraintError` is new, and `MissingAccessToken` now
|
|
49
|
+
raises lazily on first call rather than eagerly at construction. rc1 carries
|
|
50
|
+
the full 0.x → 1.0 mapping. (rc1)
|
|
51
|
+
- **Collection-returning methods return `Fortnox::Collection`, not `Array`.**
|
|
52
|
+
It is `Enumerable` and delegates the common Array methods, so most usage is
|
|
53
|
+
unchanged; `is_a?(Array)` checks and `==` against Array literals are not. (rc1)
|
|
54
|
+
- **`Invoice#accounting_method` and `#invoice_type` are enums**, where 0.x
|
|
55
|
+
took any string client-side. (rc1)
|
|
56
|
+
- **String attributes normalise `''` to `nil`.** Unset string attributes now
|
|
57
|
+
read as `nil`, never `''`, so comparisons against `''` must become `nil`
|
|
58
|
+
checks. This is what makes clearing a field work at all — Fortnox ignores
|
|
59
|
+
empty strings in updates — and it means updating a required string
|
|
60
|
+
attribute to `''` raises `Fortnox::MissingAttributeError` before any
|
|
61
|
+
request goes out. Enum attributes whose value set genuinely includes `''`
|
|
62
|
+
are exempt. (rc13)
|
|
63
|
+
- **Unknown attributes raise `Fortnox::UnknownAttributeError`** on `new`,
|
|
64
|
+
`stub` and `update`, where 0.x discarded them silently — a typo cost a
|
|
65
|
+
field with nothing to show for it. Code passing a wider hash than the
|
|
66
|
+
resource declares must slice it first. Parsing an API response stays
|
|
67
|
+
tolerant of undeclared fields. (rc14)
|
|
68
|
+
|
|
11
69
|
## [1.0.0.rc16] - 2026-08-19
|
|
12
70
|
|
|
13
71
|
### Added
|
|
@@ -436,7 +494,8 @@ for the full list of breaking changes.
|
|
|
436
494
|
For changes prior to the 1.0 rewrite, see the
|
|
437
495
|
[0.x changelog](https://github.com/accodeing/fortnox-api/blob/v0.9.2/CHANGELOG.md).
|
|
438
496
|
|
|
439
|
-
[Unreleased]: https://github.com/accodeing/fortnox-api/compare/v1.0.0
|
|
497
|
+
[Unreleased]: https://github.com/accodeing/fortnox-api/compare/v1.0.0...HEAD
|
|
498
|
+
[1.0.0]: https://github.com/accodeing/fortnox-api/compare/v1.0.0.rc16...v1.0.0
|
|
440
499
|
[1.0.0.rc16]: https://github.com/accodeing/fortnox-api/compare/v1.0.0.rc15...v1.0.0.rc16
|
|
441
500
|
[1.0.0.rc15]: https://github.com/accodeing/fortnox-api/compare/v1.0.0.rc14...v1.0.0.rc15
|
|
442
501
|
[1.0.0.rc14]: https://github.com/accodeing/fortnox-api/compare/v1.0.0.rc13...v1.0.0.rc14
|
data/MIGRATING_TO_1.0.md
ADDED
|
@@ -0,0 +1,518 @@
|
|
|
1
|
+
# Migrating to v1.0
|
|
2
|
+
|
|
3
|
+
Version 1.0 is a complete rewrite. The gem is now built on
|
|
4
|
+
[rest-easy](https://github.com/accodeing/rest-easy), replacing the old HTTParty + Data Mapper
|
|
5
|
+
architecture.
|
|
6
|
+
|
|
7
|
+
This guide covers the changes you need to make to upgrade your code. For the
|
|
8
|
+
complete list of changes — including non-breaking improvements and bug fixes
|
|
9
|
+
— see [CHANGELOG.md](CHANGELOG.md).
|
|
10
|
+
|
|
11
|
+
This guide compares against the final pre-1.0 release line, 0.9 (0.9.2
|
|
12
|
+
being the last release when this is written). References to "0.9" below mean that line
|
|
13
|
+
— earlier 0.x releases are not covered. For the old code and documentation, see the
|
|
14
|
+
[v0.9.2 release](https://github.com/accodeing/fortnox-api/tree/v0.9.2).
|
|
15
|
+
|
|
16
|
+
## Ruby version
|
|
17
|
+
|
|
18
|
+
The minimum Ruby version is now 3.2.
|
|
19
|
+
|
|
20
|
+
## Require path
|
|
21
|
+
|
|
22
|
+
```ruby
|
|
23
|
+
# Before
|
|
24
|
+
require 'fortnox/api'
|
|
25
|
+
|
|
26
|
+
# After
|
|
27
|
+
require 'fortnox'
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
## Authorization
|
|
31
|
+
|
|
32
|
+
The gem now uses the Fortnox client credentials flow. Refresh tokens are no
|
|
33
|
+
longer supported. A tenant ID is now required — this identifies which Fortnox
|
|
34
|
+
company/tenant you are connecting to.
|
|
35
|
+
|
|
36
|
+
If you don't have a tenant ID yet, run `fortnox-setup` to perform the one-time
|
|
37
|
+
OAuth authorization and retrieve it. See the [README](README.md#authorization)
|
|
38
|
+
for details.
|
|
39
|
+
|
|
40
|
+
```ruby
|
|
41
|
+
# Before
|
|
42
|
+
tokens = Fortnox::API::Repository::Authentication.new.renew_tokens(
|
|
43
|
+
refresh_token: 'your-refresh-token',
|
|
44
|
+
client_id: 'your-client-id',
|
|
45
|
+
client_secret: 'your-client-secret'
|
|
46
|
+
)
|
|
47
|
+
Fortnox::API.access_token = tokens[:access_token]
|
|
48
|
+
|
|
49
|
+
# After
|
|
50
|
+
token = Fortnox.request_access_token(
|
|
51
|
+
client_id: 'your-client-id',
|
|
52
|
+
client_secret: 'your-client-secret',
|
|
53
|
+
tenant_id: 'your-tenant-id'
|
|
54
|
+
)
|
|
55
|
+
Fortnox.access_token = token
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
## Environment variables
|
|
59
|
+
|
|
60
|
+
If you read credentials from environment variables (for example via
|
|
61
|
+
`fortnox-update-env` or your own loader), the variable names changed —
|
|
62
|
+
the `_API_` infix is gone:
|
|
63
|
+
|
|
64
|
+
- `FORTNOX_API_CLIENT_ID` → `FORTNOX_CLIENT_ID`
|
|
65
|
+
- `FORTNOX_API_CLIENT_SECRET` → `FORTNOX_CLIENT_SECRET`
|
|
66
|
+
- `FORTNOX_API_ACCESS_TOKEN` → `FORTNOX_ACCESS_TOKEN`
|
|
67
|
+
|
|
68
|
+
Removed:
|
|
69
|
+
|
|
70
|
+
- `FORTNOX_API_REFRESH_TOKEN` — refresh tokens are no longer supported
|
|
71
|
+
- `FORTNOX_API_REDIRECT_URI` — passed interactively to `fortnox-setup`
|
|
72
|
+
- `FORTNOX_API_SCOPES` — selected interactively in `fortnox-setup`
|
|
73
|
+
|
|
74
|
+
New:
|
|
75
|
+
|
|
76
|
+
- `FORTNOX_TENANT_ID` — required for the client credentials flow
|
|
77
|
+
|
|
78
|
+
## Resources replace repositories
|
|
79
|
+
|
|
80
|
+
The separate model, type, mapper, and repository classes have been replaced by
|
|
81
|
+
a single resource class per entity. The top-level namespace moves from
|
|
82
|
+
`Fortnox::API` to `Fortnox`.
|
|
83
|
+
|
|
84
|
+
```ruby
|
|
85
|
+
# Before
|
|
86
|
+
repo = Fortnox::API::Repository::Customer.new
|
|
87
|
+
repo.all
|
|
88
|
+
repo.find(1)
|
|
89
|
+
repo.save(customer)
|
|
90
|
+
|
|
91
|
+
# After
|
|
92
|
+
Fortnox::Customer.all
|
|
93
|
+
Fortnox::Customer.find(1)
|
|
94
|
+
Fortnox::Customer.save(customer)
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
## Access token timing
|
|
98
|
+
|
|
99
|
+
In 0.9 the access token was checked when you constructed a repository.
|
|
100
|
+
`Fortnox::API::Repository::Customer.new` raised
|
|
101
|
+
`Fortnox::API::MissingAccessToken` immediately if the thread had no token
|
|
102
|
+
set, before any HTTP call.
|
|
103
|
+
|
|
104
|
+
1.x has no repositories to construct (see
|
|
105
|
+
[Resources replace repositories](#resources-replace-repositories)). The
|
|
106
|
+
token is now checked lazily by the thread-local authentication, on the
|
|
107
|
+
first API call from a thread that has no token set:
|
|
108
|
+
|
|
109
|
+
```ruby
|
|
110
|
+
# Before — raised at construction
|
|
111
|
+
repo = Fortnox::API::Repository::Customer.new # => Fortnox::API::MissingAccessToken
|
|
112
|
+
|
|
113
|
+
# After — nothing to construct; the first call raises
|
|
114
|
+
Fortnox::Customer.find(1) # => Fortnox::MissingAccessToken
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
Two consequences when migrating:
|
|
118
|
+
|
|
119
|
+
- **Rescue location moves.** Code that wrapped repository construction
|
|
120
|
+
(at boot, or in an initializer) with `rescue
|
|
121
|
+
Fortnox::API::MissingAccessToken` will find that catch path stops
|
|
122
|
+
firing — the failure now surfaces at the first API call. Move the
|
|
123
|
+
rescue to the call site.
|
|
124
|
+
- **Each thread still needs its own token, and mis-setup surfaces
|
|
125
|
+
later.** The token is thread-local (this was true in 0.9 too): Sidekiq
|
|
126
|
+
workers, Puma threads, etc. must each set `Fortnox.access_token = …`
|
|
127
|
+
before their first API call. A thread that forgets no longer fails
|
|
128
|
+
early at construction — it fails on its first request, so the mistake
|
|
129
|
+
shows up at the call site rather than at boot.
|
|
130
|
+
|
|
131
|
+
## Return values
|
|
132
|
+
|
|
133
|
+
`find`, `save`, and `all` now return resource instances instead of model
|
|
134
|
+
objects. Attributes are accessed directly on the instance, so existing reads
|
|
135
|
+
generally keep working:
|
|
136
|
+
|
|
137
|
+
```ruby
|
|
138
|
+
# Before
|
|
139
|
+
invoice = repo.find(1)
|
|
140
|
+
invoice.total # => 100.0
|
|
141
|
+
invoice.customer_name # => 'Acme'
|
|
142
|
+
|
|
143
|
+
# After
|
|
144
|
+
invoice = Fortnox::Invoice.find(1)
|
|
145
|
+
invoice.total # => 100.0
|
|
146
|
+
invoice.customer_name # => 'Acme'
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
Resource instances also have `.meta` (tracks whether the record is new or
|
|
150
|
+
saved) and `.unique_id` (the primary key).
|
|
151
|
+
|
|
152
|
+
`all`, `find(hash)`, `search`, and `only` return a `Fortnox::Collection` of
|
|
153
|
+
resource instances. Collection is `Enumerable` and delegates `each`, `first`,
|
|
154
|
+
`last`, `size`, `length`, `empty?`, `[]`, and `to_a`, so iteration and most
|
|
155
|
+
existing Array usage works unchanged. Each element is a resource, not a
|
|
156
|
+
model. Collections also expose pagination metadata:
|
|
157
|
+
|
|
158
|
+
```ruby
|
|
159
|
+
customers = Fortnox::Customer.all
|
|
160
|
+
customers.first.name # => 'Acme'
|
|
161
|
+
customers.total # => 327
|
|
162
|
+
customers.pages # => 7
|
|
163
|
+
customers.current_page # => 1
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
Code that explicitly checks `is_a?(Array)` or compares with `==` against an
|
|
167
|
+
Array literal needs updating.
|
|
168
|
+
|
|
169
|
+
## Creating and updating
|
|
170
|
+
|
|
171
|
+
Use `stub` to create a new unsaved instance. `new` is not used directly.
|
|
172
|
+
|
|
173
|
+
```ruby
|
|
174
|
+
# Before
|
|
175
|
+
customer = Fortnox::API::Model::Customer.new(name: 'Acme')
|
|
176
|
+
saved = repo.save(customer)
|
|
177
|
+
|
|
178
|
+
# After
|
|
179
|
+
customer = Fortnox::Customer.stub(name: 'Acme')
|
|
180
|
+
saved = Fortnox::Customer.save(customer)
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
Updating works the same way — call `update` on a saved instance, then save:
|
|
184
|
+
|
|
185
|
+
```ruby
|
|
186
|
+
updated = saved.update(name: 'Acme Inc')
|
|
187
|
+
Fortnox::Customer.save(updated)
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
## Nested models
|
|
191
|
+
|
|
192
|
+
Nested models like invoice rows are now `Dry::Struct` subclasses under
|
|
193
|
+
`Fortnox::Structs`:
|
|
194
|
+
|
|
195
|
+
```ruby
|
|
196
|
+
# Before
|
|
197
|
+
row = Fortnox::API::Types::InvoiceRow.new(article_number: '101', price: 10)
|
|
198
|
+
invoice = Fortnox::API::Model::Invoice.new(
|
|
199
|
+
customer_number: '1',
|
|
200
|
+
invoice_rows: [row]
|
|
201
|
+
)
|
|
202
|
+
|
|
203
|
+
# After
|
|
204
|
+
row = Fortnox::Structs::InvoiceRow.new(article_number: '101', price: 10)
|
|
205
|
+
invoice = Fortnox::Invoice.stub(
|
|
206
|
+
customer_number: '1',
|
|
207
|
+
invoice_rows: [row]
|
|
208
|
+
)
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
### Nested struct gotchas
|
|
212
|
+
|
|
213
|
+
**Hash keys are the snake_case attribute names.** `stub` and `update` accept
|
|
214
|
+
plain hashes in place of struct instances and coerce them for you. Symbol and
|
|
215
|
+
string keys both work; the PascalCase Fortnox API name does not — that is the
|
|
216
|
+
wire format, and these are model attributes:
|
|
217
|
+
|
|
218
|
+
```ruby
|
|
219
|
+
Fortnox::Order.stub(customer_number: '1', order_rows: [{ article_number: '101' }])
|
|
220
|
+
Fortnox::Order.stub(customer_number: '1', order_rows: [{ 'article_number' => '101' }])
|
|
221
|
+
# both => {"Order":{"CustomerNumber":"1","OrderRows":[{"ArticleNumber":"101"}]}}
|
|
222
|
+
|
|
223
|
+
Fortnox::Order.stub(customer_number: '1', order_rows: [{ 'ArticleNumber' => '101' }])
|
|
224
|
+
# => Fortnox::UnknownAttributeError
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
0.9 ignored an attribute name it didn't recognise. 1.x raises
|
|
228
|
+
`Fortnox::UnknownAttributeError`, which carries `.attribute_names`. If you
|
|
229
|
+
relied on passing a wider hash than the resource declares — a record from
|
|
230
|
+
elsewhere in your app, say — slice it down to the attributes you mean to
|
|
231
|
+
send.
|
|
232
|
+
|
|
233
|
+
**Booleans accept the usual param spellings.** Both resource and struct
|
|
234
|
+
attributes coerce `'true'`, `'false'`, `'yes'`, `'no'`, `'1'`, `'0'`, `'on'`
|
|
235
|
+
and `'off'`, raising `Fortnox::ConstraintError` on anything else, so a
|
|
236
|
+
controller can hand string params straight through:
|
|
237
|
+
|
|
238
|
+
```ruby
|
|
239
|
+
Fortnox::Structs::OrderRow.new(housework: 'true').housework # => true
|
|
240
|
+
```
|
|
241
|
+
|
|
242
|
+
Note that the same is true one level down: a nested row hash gets the same
|
|
243
|
+
coercion as a top-level attribute, so there is no need to cast booleans
|
|
244
|
+
yourself before building rows.
|
|
245
|
+
|
|
246
|
+
## Stricter attribute validation
|
|
247
|
+
|
|
248
|
+
Several attributes that 0.9 accepted permissively are now validated client-side
|
|
249
|
+
to match the Fortnox API specification. Code that was accidentally relying on
|
|
250
|
+
the lenient 0.9 behavior will raise `Fortnox::ConstraintError` (or
|
|
251
|
+
`Fortnox::MissingAttributeError` for required fields) before the request
|
|
252
|
+
goes out.
|
|
253
|
+
|
|
254
|
+
### Country attributes
|
|
255
|
+
|
|
256
|
+
`country_code` and `delivery_country` on documents only accept ISO alpha-2
|
|
257
|
+
codes. 0.9 also accepted country names.
|
|
258
|
+
|
|
259
|
+
```ruby
|
|
260
|
+
# Before — accepted codes, Swedish names, and English names
|
|
261
|
+
invoice = Fortnox::API::Model::Invoice.new(country: 'Norge')
|
|
262
|
+
invoice = Fortnox::API::Model::Invoice.new(country: 'Norway')
|
|
263
|
+
invoice = Fortnox::API::Model::Invoice.new(country: 'NO')
|
|
264
|
+
|
|
265
|
+
# After — only ISO alpha-2 codes
|
|
266
|
+
invoice = Fortnox::Invoice.stub(country_code: 'NO')
|
|
267
|
+
```
|
|
268
|
+
|
|
269
|
+
### `Invoice.invoice_type`
|
|
270
|
+
|
|
271
|
+
Now an enum. Accepted values: `''`, `'INVOICE'`, `'AGREEMENTINVOICE'`,
|
|
272
|
+
`'INTRESTINVOICE'`, `'SUMMARYINVOICE'`, `'CASHINVOICE'`.
|
|
273
|
+
|
|
274
|
+
```ruby
|
|
275
|
+
# After
|
|
276
|
+
invoice = Fortnox::Invoice.stub(invoice_type: 'INVOICE')
|
|
277
|
+
```
|
|
278
|
+
|
|
279
|
+
### `Unit.description`
|
|
280
|
+
|
|
281
|
+
Now required. 0.9 accepted `nil` client-side, but the Fortnox API rejected
|
|
282
|
+
unset descriptions anyway — the new behavior fails earlier.
|
|
283
|
+
|
|
284
|
+
```ruby
|
|
285
|
+
# Before — accepted client-side, rejected by the API
|
|
286
|
+
unit = Fortnox::API::Model::Unit.new(code: 'PCS')
|
|
287
|
+
|
|
288
|
+
# After — must include description
|
|
289
|
+
unit = Fortnox::Unit.stub(code: 'PCS', description: 'Pieces')
|
|
290
|
+
```
|
|
291
|
+
|
|
292
|
+
## Update payloads
|
|
293
|
+
|
|
294
|
+
The dirty-tracking model changed from value-level to field-level.
|
|
295
|
+
|
|
296
|
+
In 0.9 the mapper computed a diff between the updated entity and the
|
|
297
|
+
originally-loaded record and sent only attributes whose **value** had
|
|
298
|
+
actually changed. A side effect of that diff was a bug: `nil` was
|
|
299
|
+
stripped before the comparison, so `update(attr: nil)` silently re-sent
|
|
300
|
+
the original value instead of clearing the field.
|
|
301
|
+
|
|
302
|
+
In 1.x the change set is the set of attributes you **pass to `.update`**,
|
|
303
|
+
regardless of whether the value differs from the stored record. Every
|
|
304
|
+
attribute in that set is sent on save; attributes you don't pass are not
|
|
305
|
+
sent and are left untouched on the record. Practical consequences:
|
|
306
|
+
|
|
307
|
+
- Passing an attribute equal to its current value still sends it (a
|
|
308
|
+
harmless no-op write on Fortnox's side). PUT bodies are therefore
|
|
309
|
+
somewhat larger than in 0.9.
|
|
310
|
+
- `nil` is now a real change: `update(attr: nil)` sends `null` and
|
|
311
|
+
clears the field. This is the bug fix the old behavior masked.
|
|
312
|
+
- Saving a persisted record **without** calling `.update` sends nothing
|
|
313
|
+
— it is a no-op rather than a full-record PUT.
|
|
314
|
+
|
|
315
|
+
```ruby
|
|
316
|
+
invoice = Fortnox::Invoice.find(1)
|
|
317
|
+
updated = invoice.update(comments: nil)
|
|
318
|
+
Fortnox::Invoice.save(updated)
|
|
319
|
+
# comments is now cleared in Fortnox
|
|
320
|
+
|
|
321
|
+
# No .update call → nothing changed → save is a no-op, no request sent
|
|
322
|
+
Fortnox::Invoice.save(Fortnox::Invoice.find(1))
|
|
323
|
+
```
|
|
324
|
+
|
|
325
|
+
## Exceptions
|
|
326
|
+
|
|
327
|
+
Every exception moved from the `Fortnox::API` namespace to `Fortnox`, and
|
|
328
|
+
the base class was renamed. Rescue `Fortnox::Error` to catch anything the
|
|
329
|
+
gem raises.
|
|
330
|
+
|
|
331
|
+
| 0.9 | 1.x |
|
|
332
|
+
| ------------------------------------- | ---------------------------------- |
|
|
333
|
+
| `Fortnox::API::Exception` (base) | `Fortnox::Error` (base) |
|
|
334
|
+
| `Fortnox::API::AttributeError` | `Fortnox::AttributeError` |
|
|
335
|
+
| `Fortnox::API::RemoteServerError` | `Fortnox::RequestError` |
|
|
336
|
+
| `Fortnox::API::MissingAttributeError` | `Fortnox::MissingAttributeError` |
|
|
337
|
+
| `Fortnox::API::MissingAccessToken` | `Fortnox::MissingAccessToken` |
|
|
338
|
+
| `Fortnox::API::MissingConfiguration` | *removed* — no equivalent |
|
|
339
|
+
| *(none)* | `Fortnox::ConstraintError` *(new)* |
|
|
340
|
+
|
|
341
|
+
### `Fortnox::ConstraintError` (new)
|
|
342
|
+
|
|
343
|
+
A subclass of `Fortnox::AttributeError`, raised by the client-side
|
|
344
|
+
coercion layer when an attribute value violates a type constraint (size,
|
|
345
|
+
format, enum, …) — before the request is sent. Because of the stricter
|
|
346
|
+
validation described in
|
|
347
|
+
[Stricter attribute validation](#stricter-attribute-validation), this is
|
|
348
|
+
the exception most likely to start firing during a migration. It carries
|
|
349
|
+
`.attribute_name` (a symbol) and `.value`:
|
|
350
|
+
|
|
351
|
+
```ruby
|
|
352
|
+
begin
|
|
353
|
+
Fortnox::Invoice.stub(invoice_type: 'NOT_A_TYPE')
|
|
354
|
+
rescue Fortnox::ConstraintError => e
|
|
355
|
+
e.attribute_name # => :invoice_type
|
|
356
|
+
e.value # => "NOT_A_TYPE"
|
|
357
|
+
end
|
|
358
|
+
```
|
|
359
|
+
|
|
360
|
+
### Hierarchy change
|
|
361
|
+
|
|
362
|
+
In 0.9, `MissingAttributeError` was a sibling of `AttributeError` — both
|
|
363
|
+
sat directly under `Fortnox::API::Exception` — so `rescue
|
|
364
|
+
Fortnox::API::AttributeError` did **not** catch it. In 1.x both
|
|
365
|
+
`ConstraintError` and `MissingAttributeError` subclass
|
|
366
|
+
`Fortnox::AttributeError`, so a single rescue catches every
|
|
367
|
+
attribute-validation failure:
|
|
368
|
+
|
|
369
|
+
```ruby
|
|
370
|
+
rescue Fortnox::AttributeError => e
|
|
371
|
+
# catches ConstraintError and MissingAttributeError
|
|
372
|
+
```
|
|
373
|
+
|
|
374
|
+
`Fortnox::API::MissingConfiguration` no longer exists — configuration
|
|
375
|
+
moved to a rest-easy `configure` block — so code that rescued it is now
|
|
376
|
+
dead. `Fortnox::MissingAccessToken` also changed timing — see
|
|
377
|
+
[Access token timing](#access-token-timing).
|
|
378
|
+
|
|
379
|
+
### `Fortnox::RequestError` exposes the response
|
|
380
|
+
|
|
381
|
+
`Fortnox::RequestError` exposes the underlying response via `.response`,
|
|
382
|
+
which carries the HTTP status code and body. The old `RemoteServerError`
|
|
383
|
+
only carried the message string, so detecting specific error conditions
|
|
384
|
+
required substring-matching the (Swedish) error text. Prefer the status
|
|
385
|
+
code:
|
|
386
|
+
|
|
387
|
+
```ruby
|
|
388
|
+
# Before — substring-match the message text
|
|
389
|
+
rescue Fortnox::API::RemoteServerError => e
|
|
390
|
+
not_found = e.message.include?('Kan inte hitta')
|
|
391
|
+
|
|
392
|
+
# After — match the HTTP status code
|
|
393
|
+
rescue Fortnox::RequestError => e
|
|
394
|
+
not_found = e.response&.status == 404
|
|
395
|
+
```
|
|
396
|
+
|
|
397
|
+
## Debugging and logging
|
|
398
|
+
|
|
399
|
+
The 0.9 gem had a single combined switch:
|
|
400
|
+
|
|
401
|
+
```ruby
|
|
402
|
+
# Before
|
|
403
|
+
Fortnox::API.configure do |config|
|
|
404
|
+
config.debugging = true
|
|
405
|
+
config.logger = Logger.new($stdout)
|
|
406
|
+
end
|
|
407
|
+
```
|
|
408
|
+
|
|
409
|
+
In 1.x this splits into two unrelated knobs.
|
|
410
|
+
|
|
411
|
+
For HTTP wire logging — request/response lines and headers, with the
|
|
412
|
+
standard auth headers redacted — set a `Logger` on the gem-level config:
|
|
413
|
+
|
|
414
|
+
```ruby
|
|
415
|
+
# After
|
|
416
|
+
Fortnox.configure do
|
|
417
|
+
logger Logger.new($stdout)
|
|
418
|
+
end
|
|
419
|
+
```
|
|
420
|
+
|
|
421
|
+
To also log request/response bodies, opt in explicitly with `log_bodies true`.
|
|
422
|
+
|
|
423
|
+
For catching schema drift — warnings when an API response contains fields a
|
|
424
|
+
resource doesn't declare with `attr` or `ignore` — set `debug` on the
|
|
425
|
+
individual resource. This is a different feature from the old `debugging`
|
|
426
|
+
flag and is opt-in per resource:
|
|
427
|
+
|
|
428
|
+
```ruby
|
|
429
|
+
# After
|
|
430
|
+
Fortnox::Customer.configure do
|
|
431
|
+
debug true
|
|
432
|
+
end
|
|
433
|
+
```
|
|
434
|
+
|
|
435
|
+
See the [Debugging section in the README](README.md#debugging) for details.
|
|
436
|
+
|
|
437
|
+
## Dependency changes
|
|
438
|
+
|
|
439
|
+
The rewrite replaced the HTTP and configuration stack, so several gems 0.9
|
|
440
|
+
installed into your bundle are gone. If your app called into one of them
|
|
441
|
+
directly — without declaring it in its own `Gemfile` — it will fail to load
|
|
442
|
+
after the upgrade, with a `LoadError` that points at your code rather than at
|
|
443
|
+
this gem:
|
|
444
|
+
|
|
445
|
+
| Gem | 0.9 | 1.x |
|
|
446
|
+
| ---------------- | ----------- | ----------------------------------------- |
|
|
447
|
+
| `httparty` | direct dep | **gone** — replaced by `faraday` |
|
|
448
|
+
| `jwt` | direct dep | **gone** — no longer used |
|
|
449
|
+
| `dry-container` | direct dep | **gone** |
|
|
450
|
+
| `dry-types` | direct dep | still installed, transitively |
|
|
451
|
+
| `dry-configurable`| direct dep | still installed, transitively |
|
|
452
|
+
|
|
453
|
+
`httparty` is the one that bites in practice: an inline `HTTParty.get(...)`
|
|
454
|
+
somewhere unrelated to Fortnox keeps working until this gem stops supplying
|
|
455
|
+
it. `dry-types` and `dry-configurable` are still in the bundle — they come in
|
|
456
|
+
via `rest-easy` and `dry-struct` — but that is an implementation detail of
|
|
457
|
+
this gem, not a promise. Declare anything you use directly in your own
|
|
458
|
+
`Gemfile`.
|
|
459
|
+
|
|
460
|
+
## Testing with VCR
|
|
461
|
+
|
|
462
|
+
Resource paths lost their trailing slash. 0.9 declared endpoints as
|
|
463
|
+
`URI = '/customers/'` against a `https://api.fortnox.se/3/` base; 1.x
|
|
464
|
+
declares `path 'customers'` against `https://api.fortnox.se/3`:
|
|
465
|
+
|
|
466
|
+
```
|
|
467
|
+
# Before
|
|
468
|
+
https://api.fortnox.se/3/customers/
|
|
469
|
+
https://api.fortnox.se/3/customers/1/
|
|
470
|
+
|
|
471
|
+
# After
|
|
472
|
+
https://api.fortnox.se/3/customers
|
|
473
|
+
https://api.fortnox.se/3/customers/1
|
|
474
|
+
```
|
|
475
|
+
|
|
476
|
+
Every cassette your app recorded against 0.9 therefore fails to match. There
|
|
477
|
+
is no rewriting shortcut worth the effort — delete the affected cassettes and
|
|
478
|
+
re-record. Note that the request headers changed too (client credentials
|
|
479
|
+
instead of refresh tokens), so a URL-only search-and-replace would leave you
|
|
480
|
+
with cassettes that match the URL and then miss on the headers.
|
|
481
|
+
|
|
482
|
+
## Rails applications
|
|
483
|
+
|
|
484
|
+
Most of the friction in a Rails upgrade is at the integration seam rather
|
|
485
|
+
than in Fortnox behaviour itself. The two below are the day-one ones; also
|
|
486
|
+
work through [Dependency changes](#dependency-changes), since `httparty` is
|
|
487
|
+
the usual casualty in a Rails app.
|
|
488
|
+
|
|
489
|
+
### `render json:` needs the Rails integration
|
|
490
|
+
|
|
491
|
+
0.9 returned plain model objects that Rails knew how to serialise. 1.x
|
|
492
|
+
resources need one require, in an initializer:
|
|
493
|
+
|
|
494
|
+
```ruby
|
|
495
|
+
# config/initializers/fortnox.rb
|
|
496
|
+
require 'fortnox/rails'
|
|
497
|
+
```
|
|
498
|
+
|
|
499
|
+
Without it, `render json: { invoices: [...] }` doesn't call `to_json` on the
|
|
500
|
+
nested resources — ActiveSupport walks the structure calling `as_json`, falls
|
|
501
|
+
through to `Object#as_json`, and serialises instance variables into your
|
|
502
|
+
response body:
|
|
503
|
+
|
|
504
|
+
```ruby
|
|
505
|
+
render json: { invoices: [Fortnox::Invoice.find(1)] }
|
|
506
|
+
# => {"invoices":[{"api_data":{…},"model_attributes":{…},"changes":[…],"meta":{…}}]}
|
|
507
|
+
```
|
|
508
|
+
|
|
509
|
+
It covers resources, collections, and nested structs. See the
|
|
510
|
+
[README](README.md#rails) for what it renders.
|
|
511
|
+
|
|
512
|
+
### Filter params before they reach a resource
|
|
513
|
+
|
|
514
|
+
Params can be passed through as they arrive — string keys and string values
|
|
515
|
+
both work. But an attribute the resource doesn't declare now raises
|
|
516
|
+
`Fortnox::UnknownAttributeError` rather than being ignored, so a form field
|
|
517
|
+
that isn't a Fortnox attribute will take a request down. Permit and slice as
|
|
518
|
+
you would for a model. See [Nested struct gotchas](#nested-struct-gotchas).
|
data/README.md
CHANGED
|
@@ -17,8 +17,7 @@ Adding more resources is quick and easy — see the
|
|
|
17
17
|
|
|
18
18
|
## Status
|
|
19
19
|
|
|
20
|
-
Version 1.0 is a complete rewrite,
|
|
21
|
-
(`1.0.0.rc8`). It is built on
|
|
20
|
+
Version 1.0 is a complete rewrite, built on
|
|
22
21
|
[rest-easy](https://github.com/accodeing/rest-easy), replacing the old
|
|
23
22
|
HTTParty + Data Mapper architecture with a single resource class per entity.
|
|
24
23
|
Authorization uses the Fortnox client credentials flow.
|
data/fortnox.gemspec
CHANGED
|
@@ -12,9 +12,17 @@ Gem::Specification.new do |spec|
|
|
|
12
12
|
spec.version = Fortnox::VERSION.dup
|
|
13
13
|
|
|
14
14
|
spec.summary = 'Fortnox F3 REST API library, based on rest-easy.'
|
|
15
|
-
spec.description =
|
|
15
|
+
spec.description = <<~DESCRIPTION
|
|
16
|
+
Fortnox's REST API wraps every payload in a type key, spells its
|
|
17
|
+
attributes in PascalCase, and treats an empty string as "leave this
|
|
18
|
+
field alone" when you meant to clear it. This gem turns it into
|
|
19
|
+
ordinary immutable Ruby objects with typed, constrained attributes,
|
|
20
|
+
so a value Fortnox would reject raises before it costs an API call,
|
|
21
|
+
and authorization, pagination and JSON mapping are handled for you.
|
|
22
|
+
DESCRIPTION
|
|
16
23
|
spec.homepage = 'https://github.com/accodeing/fortnox'
|
|
17
|
-
spec.files = Dir['CHANGELOG.md', 'LICENSE.md', '
|
|
24
|
+
spec.files = Dir['CHANGELOG.md', 'LICENSE.md', 'MIGRATING_TO_1.0.md', 'README.md',
|
|
25
|
+
'fortnox.gemspec', 'lib/**/*']
|
|
18
26
|
spec.bindir = 'bin'
|
|
19
27
|
spec.executables = ['fortnox-setup', 'fortnox-update-env']
|
|
20
28
|
spec.require_paths = ['lib']
|
data/lib/fortnox/resource.rb
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
module Fortnox
|
|
4
4
|
# TODO: this class is still over the length limit even with error
|
|
5
|
-
# translation extracted — see
|
|
5
|
+
# translation extracted — see TODO.md.
|
|
6
6
|
# rubocop:disable Metrics/ClassLength
|
|
7
7
|
class Resource < RestEasy::Resource
|
|
8
8
|
include Fortnox::Types
|
data/lib/fortnox/version.rb
CHANGED
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
|
|
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-
|
|
14
|
+
date: 2026-09-23 00:00:00.000000000 Z
|
|
15
15
|
dependencies:
|
|
16
16
|
- !ruby/object:Gem::Dependency
|
|
17
17
|
name: base64
|
|
@@ -69,7 +69,13 @@ dependencies:
|
|
|
69
69
|
- - "~>"
|
|
70
70
|
- !ruby/object:Gem::Version
|
|
71
71
|
version: 1.4.1
|
|
72
|
-
description:
|
|
72
|
+
description: |
|
|
73
|
+
Fortnox's REST API wraps every payload in a type key, spells its
|
|
74
|
+
attributes in PascalCase, and treats an empty string as "leave this
|
|
75
|
+
field alone" when you meant to clear it. This gem turns it into
|
|
76
|
+
ordinary immutable Ruby objects with typed, constrained attributes,
|
|
77
|
+
so a value Fortnox would reject raises before it costs an API call,
|
|
78
|
+
and authorization, pagination and JSON mapping are handled for you.
|
|
73
79
|
email:
|
|
74
80
|
- info@accodeing.com
|
|
75
81
|
executables:
|
|
@@ -80,6 +86,7 @@ extra_rdoc_files: []
|
|
|
80
86
|
files:
|
|
81
87
|
- CHANGELOG.md
|
|
82
88
|
- LICENSE.md
|
|
89
|
+
- MIGRATING_TO_1.0.md
|
|
83
90
|
- README.md
|
|
84
91
|
- bin/fortnox-setup
|
|
85
92
|
- bin/fortnox-update-env
|
|
@@ -138,9 +145,9 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
|
138
145
|
version: 3.2.0
|
|
139
146
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
140
147
|
requirements:
|
|
141
|
-
- - "
|
|
148
|
+
- - ">="
|
|
142
149
|
- !ruby/object:Gem::Version
|
|
143
|
-
version:
|
|
150
|
+
version: '0'
|
|
144
151
|
requirements: []
|
|
145
152
|
rubygems_version: 3.4.19
|
|
146
153
|
signing_key:
|