fortnox-api 1.0.0.rc16 → 1.0.1

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: 7350800553373489f35a73af7206ae013a6004024b53db948e2778e85c0f61b5
4
- data.tar.gz: 359f4b9fb46a0b17aa67180b977021a9bcab6653a722043df93954ccc68b470d
3
+ metadata.gz: ee5902e0761d37969a38cf156349c22e8f3e967f0e8120f31753b17a12561e51
4
+ data.tar.gz: 4fc8b16d34cf2b2211f5587422c196f1711b4d498158a39480024117330284cb
5
5
  SHA512:
6
- metadata.gz: c9f62481c8990889206483a455a6f8b6c11060c4f60aad75c8ebbe97825efa5747f7176506a1de69e66df0d0d56592b35d713154a4a033176da5646bc8c7aa2d
7
- data.tar.gz: a2e684f757d98304b7ed7e08c167900abaecb027fb31b291b59d1fe57c5cc0f89753e3d0110d9f8cd01c90ce36964c09c28d7b85b8474ea9758507366e647672
6
+ metadata.gz: 2bbee1c33ff2a933bf3f090aea82838a61f744300f0f8ad3106d17e28a461b77c3b649141628361d9a3359a98bef33cef0e2d0c1545f922e3834df520c42da2e
7
+ data.tar.gz: 4013813c2652fedc7b8f96e31686ff6a6c62aefac25e8a8f8e1516159fa95b22386e26bd8f19f538cd0b7e882cfcdaed97b2a6a9681c4238af8885f8b484dba3
data/CHANGELOG.md CHANGED
@@ -8,6 +8,91 @@ and this project adheres to
8
8
 
9
9
  ## [Unreleased]
10
10
 
11
+ ## [1.0.1] - 2026-09-23
12
+
13
+ ### Fixed
14
+
15
+ - **Updating a record before its first save no longer drops the attributes it
16
+ was built with.** Calling `.update` on a stubbed record marked it as already
17
+ persisted, so `save` sent a PUT for a record that does not exist yet, and the
18
+ request body shrank to only the updated fields — everything passed to `stub`
19
+ never reached Fortnox. This is the bug reported in #233 against 0.x and fixed
20
+ there in 2023; the 1.0 rest-easy rewrite reintroduced it, and made it worse by
21
+ switching the verb as well as the body.
22
+ - **Chained updates no longer lose all but the last.**
23
+ `record.update(phone1: '1').update(email: 'x@y.se')` sent only `Email`, while
24
+ `phone1` still read back as `'1'` on the instance — the object and the request
25
+ body disagreed, with nothing raised. Both attributes are now sent. Introduced
26
+ by the 1.0 rewrite.
27
+ - **`meta.partial?` survives an update.** Instances parsed from a collection
28
+ response are flagged partial, and `.update` silently cleared the flag, so the
29
+ check the README recommends before re-fetching reported a shallow record as
30
+ complete the moment anything touched it. The flag is new in 1.0, so this
31
+ never affected 0.x.
32
+
33
+ ### Changed
34
+
35
+ - The `rest-easy` dependency is now `~> 1.4.2`, which carries the three fixes
36
+ above — all of them live in its `update`, not in this gem.
37
+
38
+ ## [1.0.0] - 2026-09-23
39
+
40
+ The first stable release of the 1.0 line. No code changes since
41
+ `1.0.0.rc16` — the release candidate series is over and the 1.0 API is
42
+ now settled. Consumers pinned to `1.0.0.rc16` can move to `1.0.0` as is;
43
+ from an earlier release candidate, see the entries below for what
44
+ changed in between.
45
+
46
+ ### Breaking changes
47
+
48
+ 1.0 is a complete rewrite and is **not** a drop-in replacement for 0.x.
49
+ [MIGRATING_TO_1.0.md](MIGRATING_TO_1.0.md) is the guided upgrade; this is
50
+ the checklist to read it against. Each entry names the release candidate
51
+ that introduced it, where the full reasoning and the exact before/after
52
+ live.
53
+
54
+ - **The gem is rebuilt on [rest-easy](https://github.com/accodeing/rest-easy)**,
55
+ one resource class per entity in place of HTTParty + Data Mapper, and the
56
+ namespace moves from `Fortnox::API` to `Fortnox` — `Fortnox::API::Repository::Customer`
57
+ is now `Fortnox::Customer`. (rc1)
58
+ - **Ruby 3.2 or later.** rc1 raised the floor to 3.1 and rc7 to 3.2; coming
59
+ from 0.x, 3.2 is the only number that matters. (rc1, rc7)
60
+ - **Authorization is the Fortnox client credentials flow.** Refresh tokens
61
+ are neither needed nor supported, and a tenant ID is now required — the
62
+ new `fortnox-setup` executable obtains one. (rc1)
63
+ - **Environment variables lose the `_API_` infix** (`FORTNOX_API_CLIENT_ID`
64
+ → `FORTNOX_CLIENT_ID`, and so on). `FORTNOX_API_REFRESH_TOKEN`,
65
+ `FORTNOX_API_REDIRECT_URI` and `FORTNOX_API_SCOPES` are gone;
66
+ `FORTNOX_TENANT_ID` is new and required. (rc1)
67
+ - **`Fortnox.request_access_token` replaces
68
+ `Fortnox::API::Repository::Authentication`** for token management. (rc1)
69
+ - **Configuration moves to module-level setters** such as
70
+ `Fortnox.access_token=`, from `Fortnox::API.configuration`. (rc1)
71
+ - **Country attributes accept ISO alpha-2 codes only** (`'NO'`, not
72
+ `'Norge'`), on `country_code` and `delivery_country`. (rc1)
73
+ - **Exception classes are renamed and consolidated**, `Fortnox::API::Exception`
74
+ → `Fortnox::Error` at the root. `Fortnox::API::MissingConfiguration` is
75
+ removed, `Fortnox::ConstraintError` is new, and `MissingAccessToken` now
76
+ raises lazily on first call rather than eagerly at construction. rc1 carries
77
+ the full 0.x → 1.0 mapping. (rc1)
78
+ - **Collection-returning methods return `Fortnox::Collection`, not `Array`.**
79
+ It is `Enumerable` and delegates the common Array methods, so most usage is
80
+ unchanged; `is_a?(Array)` checks and `==` against Array literals are not. (rc1)
81
+ - **`Invoice#accounting_method` and `#invoice_type` are enums**, where 0.x
82
+ took any string client-side. (rc1)
83
+ - **String attributes normalise `''` to `nil`.** Unset string attributes now
84
+ read as `nil`, never `''`, so comparisons against `''` must become `nil`
85
+ checks. This is what makes clearing a field work at all — Fortnox ignores
86
+ empty strings in updates — and it means updating a required string
87
+ attribute to `''` raises `Fortnox::MissingAttributeError` before any
88
+ request goes out. Enum attributes whose value set genuinely includes `''`
89
+ are exempt. (rc13)
90
+ - **Unknown attributes raise `Fortnox::UnknownAttributeError`** on `new`,
91
+ `stub` and `update`, where 0.x discarded them silently — a typo cost a
92
+ field with nothing to show for it. Code passing a wider hash than the
93
+ resource declares must slice it first. Parsing an API response stays
94
+ tolerant of undeclared fields. (rc14)
95
+
11
96
  ## [1.0.0.rc16] - 2026-08-19
12
97
 
13
98
  ### Added
@@ -436,7 +521,9 @@ for the full list of breaking changes.
436
521
  For changes prior to the 1.0 rewrite, see the
437
522
  [0.x changelog](https://github.com/accodeing/fortnox-api/blob/v0.9.2/CHANGELOG.md).
438
523
 
439
- [Unreleased]: https://github.com/accodeing/fortnox-api/compare/v1.0.0.rc16...HEAD
524
+ [Unreleased]: https://github.com/accodeing/fortnox-api/compare/v1.0.1...HEAD
525
+ [1.0.1]: https://github.com/accodeing/fortnox-api/compare/v1.0.0...v1.0.1
526
+ [1.0.0]: https://github.com/accodeing/fortnox-api/compare/v1.0.0.rc16...v1.0.0
440
527
  [1.0.0.rc16]: https://github.com/accodeing/fortnox-api/compare/v1.0.0.rc15...v1.0.0.rc16
441
528
  [1.0.0.rc15]: https://github.com/accodeing/fortnox-api/compare/v1.0.0.rc14...v1.0.0.rc15
442
529
  [1.0.0.rc14]: https://github.com/accodeing/fortnox-api/compare/v1.0.0.rc13...v1.0.0.rc14
@@ -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, currently in release candidate
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 = spec.summary
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', 'README.md', 'fortnox.gemspec', 'lib/**/*']
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']
@@ -24,7 +32,7 @@ Gem::Specification.new do |spec|
24
32
  spec.add_dependency 'base64', '~> 0.2'
25
33
  spec.add_dependency 'countries', '~> 7.1'
26
34
  spec.add_dependency 'dry-struct', '~> 1.5'
27
- spec.add_dependency 'rest-easy', '~> 1.4.1'
35
+ spec.add_dependency 'rest-easy', '~> 1.4.2'
28
36
 
29
37
  spec.metadata['rubygems_mfa_required'] = 'true'
30
38
  end
@@ -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 todo.md.
5
+ # translation extracted — see TODO.md.
6
6
  # rubocop:disable Metrics/ClassLength
7
7
  class Resource < RestEasy::Resource
8
8
  include Fortnox::Types
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Fortnox
4
- VERSION = '1.0.0.rc16'
4
+ VERSION = '1.0.1'
5
5
  end
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.rc16
4
+ version: 1.0.1
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-08-19 00:00:00.000000000 Z
14
+ date: 2026-09-23 00:00:00.000000000 Z
15
15
  dependencies:
16
16
  - !ruby/object:Gem::Dependency
17
17
  name: base64
@@ -61,15 +61,21 @@ dependencies:
61
61
  requirements:
62
62
  - - "~>"
63
63
  - !ruby/object:Gem::Version
64
- version: 1.4.1
64
+ version: 1.4.2
65
65
  type: :runtime
66
66
  prerelease: false
67
67
  version_requirements: !ruby/object:Gem::Requirement
68
68
  requirements:
69
69
  - - "~>"
70
70
  - !ruby/object:Gem::Version
71
- version: 1.4.1
72
- description: Fortnox F3 REST API library, based on rest-easy.
71
+ version: 1.4.2
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: 1.3.1
150
+ version: '0'
144
151
  requirements: []
145
152
  rubygems_version: 3.4.19
146
153
  signing_key: