fortnox-api 1.0.0.rc6 → 1.0.0.rc9

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: 42db7fb64630302bc5d10e1ea99f03586a6d6927ed09a33bdbce1408fb820684
4
- data.tar.gz: a98ed1b9a2380a48044f1b6f29f99ef4da35d92b9b4a7d70a260a0e82f244f60
3
+ metadata.gz: a20aeb0e4dd1618609c445e649bc8b1d5014febdc7eafcb733d4477a054dca60
4
+ data.tar.gz: '02592cebf00ea6190f2d0d863a54441346e973e06e0f65fd4e436cda63c57587'
5
5
  SHA512:
6
- metadata.gz: 67396cd4f4f0609671f11d30195446514208e3e57a244071a144151daca2b40d988167bf2ea73e4cb256d425feaa20e7b92c2450cbbc6d85fbb2cea61e7112ae
7
- data.tar.gz: ebd081caa219cb82d91a15ba6c6f58c62f6e19f4fc789ffe8963bca9ae533f1138766bbb1ec7fe6f4f1550fa2a0416bf6d7a37fa63ea0642833ed45d4d91f3c7
6
+ metadata.gz: d65bbab5d39d6428068de1132c2e5d0251acfd3c4e70fe120d37fbb0680d6e82f861935106e6386c4b3b2951f8a513b19b821009459a6e5c4a69f38a9368d50f
7
+ data.tar.gz: 95858ca85e8694c9d699ec7dd054f5c93e7a1fce3ca078d8bb690ee61f0e691a44cff23f4c3a5633459659234560815a766391ca2d12dce3b1f804f54abb486f
data/CHANGELOG.md CHANGED
@@ -6,6 +6,56 @@ 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
+ ## [Unreleased]
10
+
11
+ ## [1.0.0.rc9] - 2026-05-20
12
+
13
+ ### Added
14
+
15
+ - `Fortnox::ALLOWED_CHARACTERS_REGEXP` constant, exposing the character
16
+ set Fortnox accepts in text fields. Sourced from the official Fortnox
17
+ docs. Useful for pre-validating strings before sending them to the API.
18
+
19
+ ### Changed
20
+
21
+ - `Customer#default_delivery_types.invoice` now accepts `'ELECTRONICINVOICE'`
22
+ when returned by Fortnox. The value is read-only — attempting to set it from
23
+ consumer code raises `Fortnox::ConstraintError` at save time,
24
+ rather than letting Fortnox reject the request.
25
+ Unchanged values round-trip through `save` as before.
26
+
27
+ ## [1.0.0.rc8] - 2026-05-19
28
+
29
+ ### Changed
30
+
31
+ - `fortnox-setup` now prints the authorization URL and asks before
32
+ opening a browser when a non-localhost redirect URI is used, defaulting
33
+ to not opening it. Previously it always opened the URL locally, so the
34
+ browser immediately followed the redirect and the URL was lost — making
35
+ it impossible to hand off to whoever should log in (e.g. a customer).
36
+ The local-server flow is unchanged.
37
+
38
+ ## [1.0.0.rc7] - 2026-05-19
39
+
40
+ ### Changed
41
+
42
+ - **Breaking** Minimum Ruby version raised to 3.2. Ruby 3.1 reached
43
+ end-of-life in March 2025 and is no longer supported or tested.
44
+ - Upgraded `rest-easy` dependency to `~> 1.3.0`.
45
+
46
+ ### Fixed
47
+
48
+ - Declare `base64` as a runtime dependency. It is `require`d by the gem
49
+ and used for OAuth credential encoding, but `base64` was removed from
50
+ Ruby's default gems in 3.4, so the gem failed to load on Ruby 3.4 with
51
+ `LoadError: cannot load such file -- base64`.
52
+ - Saving a persisted record with no changes is again a no-op, matching
53
+ 0.9. In that version `save` returned early for an unchanged persisted record
54
+ without issuing a request. The 1.0.0.rc1 rest-easy rewrite regressed
55
+ this: `save` on a record fetched via `find` re-sent every attribute
56
+ via a full-record `PUT`, which could clobber fields changed elsewhere
57
+ since it was loaded.
58
+
9
59
  ## [1.0.0.rc6] - 2026-05-18
10
60
 
11
61
  ### Added
@@ -194,6 +244,12 @@ for the full list of breaking changes.
194
244
  For changes prior to the 1.0 rewrite, see the
195
245
  [0.x changelog](https://github.com/accodeing/fortnox-api/blob/v0.9.2/CHANGELOG.md).
196
246
 
247
+ [Unreleased]: https://github.com/accodeing/fortnox-api/compare/v1.0.0.rc9...HEAD
248
+ [1.0.0.rc9]: https://github.com/accodeing/fortnox-api/compare/v1.0.0.rc8...v1.0.0.rc9
249
+ [1.0.0.rc8]: https://github.com/accodeing/fortnox-api/compare/v1.0.0.rc7...v1.0.0.rc8
250
+ [1.0.0.rc7]: https://github.com/accodeing/fortnox-api/compare/v1.0.0.rc6...v1.0.0.rc7
251
+ [1.0.0.rc6]: https://github.com/accodeing/fortnox-api/compare/v1.0.0.rc5...v1.0.0.rc6
252
+ [1.0.0.rc5]: https://github.com/accodeing/fortnox-api/compare/v1.0.0.rc4...v1.0.0.rc5
197
253
  [1.0.0.rc4]: https://github.com/accodeing/fortnox-api/compare/v1.0.0.rc3...v1.0.0.rc4
198
254
  [1.0.0.rc3]: https://github.com/accodeing/fortnox-api/compare/v1.0.0.rc2...v1.0.0.rc3
199
255
  [1.0.0.rc2]: https://github.com/accodeing/fortnox-api/compare/v1.0.0.rc1...v1.0.0.rc2
data/README.md CHANGED
@@ -1,5 +1,7 @@
1
1
  # fortnox-api
2
2
 
3
+ [![CI](https://github.com/accodeing/fortnox-api/actions/workflows/ci.yml/badge.svg)](https://github.com/accodeing/fortnox-api/actions/workflows/ci.yml)
4
+
3
5
  Ruby gem for Fortnox's version 3 REST API, built on
4
6
  [rest-easy](https://github.com/accodeing/rest-easy). If you need to integrate an existing or new Ruby
5
7
  or Rails app against Fortnox this gem will save you a lot of time.
@@ -10,13 +12,13 @@ Feel free to repay the community with some nice PRs of your own.
10
12
 
11
13
  Article, Customer, Invoice, Label, Order, Project, TermsOfPayment, Unit
12
14
 
13
- Adding more resources is quick and easy, see the
14
- [Contributing](#contributing) section.
15
+ Adding more resources is quick and easy see the
16
+ [Developer readme](DEVELOPER_README.md).
15
17
 
16
18
  ## Status
17
19
 
18
20
  Version 1.0 is a complete rewrite, currently in release candidate
19
- (`1.0.0.rc6`). It is built on
21
+ (`1.0.0.rc8`). It is built on
20
22
  [rest-easy](https://github.com/accodeing/rest-easy), replacing the old
21
23
  HTTParty + Data Mapper architecture with a single resource class per entity.
22
24
  Authorization uses the Fortnox client credentials flow.
@@ -62,6 +64,16 @@ sending it to the API, which saves you API calls and time debugging. You can
62
64
  still get errors from the server; our implementation is not perfect. Also,
63
65
  Fortnox sometimes requires a specific combination of attributes.
64
66
 
67
+ #### Allowed characters
68
+
69
+ `Fortnox::ALLOWED_CHARACTERS_REGEXP` exposes the character set Fortnox
70
+ accepts in text fields, sourced from the [official Fortnox docs][formats].
71
+ The gem itself does not apply this regex to attribute values — it is
72
+ provided so consumers can pre-validate or sanitize strings before
73
+ sending them to the API.
74
+
75
+ [formats]: https://www.fortnox.se/developer/guides-and-good-to-know/formats-and-encoding
76
+
65
77
  #### Exceptions
66
78
 
67
79
  The gem raises the following exceptions:
@@ -71,7 +83,17 @@ The gem raises the following exceptions:
71
83
  - `Fortnox::RequestError` — 4xx/5xx responses from the Fortnox API. The
72
84
  exception message includes the API's `ErrorInformation.Message` and code
73
85
  when present (Fortnox is inconsistent about the key casing — the gem
74
- normalises both). The full response is on `.response`.
86
+ normalises both). The full response is on `.response`, which exposes
87
+ the HTTP status code and body — match on `.status` rather than the
88
+ message text:
89
+
90
+ ```ruby
91
+ Fortnox::Customer.find(id)
92
+ rescue Fortnox::RequestError => e
93
+ return nil if e.response&.status == 404
94
+
95
+ raise
96
+ ```
75
97
  - `Fortnox::AttributeError` — base for attribute validation failures.
76
98
  - `Fortnox::ConstraintError` — an attribute value violates a type
77
99
  constraint (max size, format, etc.). Carries `.attribute_name` and
@@ -83,7 +105,7 @@ The gem raises the following exceptions:
83
105
 
84
106
  ## Requirements
85
107
 
86
- Ruby 3.1 or higher.
108
+ Ruby 3.2 or higher.
87
109
 
88
110
  ### Installation
89
111
 
@@ -308,6 +330,19 @@ updated = customer.update(name: 'Acme Inc')
308
330
  Fortnox::Customer.save(updated)
309
331
  ```
310
332
 
333
+ Updates are partial: only the attributes you pass to `.update` are sent to
334
+ Fortnox. Attributes you don't touch are left as-is on the record (so a
335
+ field changed elsewhere in the meantime is not clobbered). Note this is
336
+ based on *which* attributes you pass, not a value comparison — passing an
337
+ attribute equal to its stored value still sends it. Setting an attribute
338
+ to `nil` is sent as an explicit clear:
339
+
340
+ ```ruby
341
+ customer = Fortnox::Customer.find(1)
342
+ updated = customer.update(phone1: nil)
343
+ Fortnox::Customer.save(updated) # phone1 is now cleared in Fortnox
344
+ ```
345
+
311
346
  ### Searching
312
347
 
313
348
  ```ruby
@@ -322,11 +357,7 @@ Some resources support server-side filters:
322
357
  Fortnox::Invoice.only('unpaid')
323
358
  ```
324
359
 
325
- ### Debugging
326
-
327
- The gem exposes two independent debugging knobs from rest-easy.
328
-
329
- #### HTTP wire logging
360
+ ### Logging
330
361
 
331
362
  Set a `Logger`-compatible instance on the gem-level config to log every
332
363
  HTTP request and response. Faraday's built-in logger middleware is attached
@@ -358,18 +389,10 @@ take effect only on restart.
358
389
  The `fortnox-setup` and `fortnox-update-env` executables talk directly to
359
390
  the OAuth token endpoint and are not routed through this logger.
360
391
 
361
- #### Per-resource response-shape validation
362
-
363
- Set `debug true` on a resource to have rest-easy warn whenever an API
364
- response contains fields the resource doesn't declare with `attr` or
365
- `ignore`, or is missing a declared (non-required) attribute. Useful for
366
- catching schema drift when Fortnox adds or renames fields:
367
-
368
- ```ruby
369
- Fortnox::Customer.configure do
370
- debug true
371
- end
372
- ```
392
+ If you suspect the gem is silently dropping a field Fortnox added — or
393
+ hitting some other schema-drift issue — please open an issue. There's a
394
+ maintainer-only knob to surface those warnings; see the
395
+ [Developer readme](DEVELOPER_README.md) for details.
373
396
 
374
397
  ### Gotchas
375
398
 
data/bin/fortnox-setup CHANGED
@@ -154,15 +154,21 @@ auth_params = URI.encode_www_form(
154
154
  authorize_url = "#{OAUTH_ENDPOINT}/auth?#{auth_params}"
155
155
 
156
156
  puts
157
- puts 'Opening browser for Fortnox authorization...'
158
-
159
- open_browser(authorize_url)
160
157
 
161
158
  if use_local_server
159
+ puts 'Opening browser for Fortnox authorization...'
160
+ open_browser(authorize_url)
162
161
  puts
163
162
  puts 'Waiting for Fortnox to redirect back...'
164
163
  authorization_code = get_auth_code_from_server(LOCAL_PORT, nonce)
165
164
  else
165
+ puts 'Authorization URL. Open it to log in, or send it to whoever should'
166
+ puts 'log in to the Fortnox account being connected:'
167
+ puts
168
+ puts " #{authorize_url}"
169
+ puts
170
+ open_here = prompt('Open it in your browser now?', default: 'n')
171
+ open_browser(authorize_url) if open_here.downcase.start_with?('y')
166
172
  authorization_code = auth_code_manually
167
173
  end
168
174
 
data/fortnox.gemspec CHANGED
@@ -19,11 +19,12 @@ Gem::Specification.new do |spec|
19
19
  spec.executables = ['fortnox-setup', 'fortnox-update-env']
20
20
  spec.require_paths = ['lib']
21
21
 
22
- spec.required_ruby_version = '>= 3.1.0'
22
+ spec.required_ruby_version = '>= 3.2.0'
23
23
 
24
+ spec.add_dependency 'base64'
24
25
  spec.add_dependency 'countries', '~> 7.1'
25
26
  spec.add_dependency 'dry-struct', '~> 1.5'
26
- spec.add_dependency 'rest-easy', '~> 1.2.0'
27
+ spec.add_dependency 'rest-easy', '~> 1.3.0'
27
28
 
28
29
  spec.metadata['rubygems_mfa_required'] = 'true'
29
30
  end
@@ -29,7 +29,9 @@ module Fortnox
29
29
  # Strip nils for new records — rely on Fortnox defaults
30
30
  data = data.compact
31
31
  elsif __changes__.any?
32
- # Only send changed attributes on update, preserving explicit nils
32
+ # Send only the attributes passed to .update field-level dirty
33
+ # tracking, not a value diff, so an attribute equal to its stored
34
+ # value is still sent, and an explicit nil reaches Fortnox as a clear.
33
35
  changed_api_names = __changes__.keys.filter_map do |model_name|
34
36
  self.class.all_attribute_definitions[model_name]&.api_name
35
37
  end
@@ -61,6 +63,12 @@ module Fortnox
61
63
  end
62
64
 
63
65
  def save(instance)
66
+ # A persisted record with no recorded changes has nothing to write.
67
+ # Without this short-circuit rest-easy would PUT the entire record
68
+ # back, re-sending every untouched attribute and risking clobbering
69
+ # changes made elsewhere since it was loaded.
70
+ return instance if !instance.meta.new? && instance.__changes__.empty?
71
+
64
72
  with_translated_errors { super }
65
73
  end
66
74
 
@@ -11,6 +11,10 @@ module Fortnox
11
11
  scope 'customer'
12
12
  end
13
13
 
14
+ before_serialise do |_attrs|
15
+ validate_electronic_invoice_delivery_type!(default_delivery_types&.invoice)
16
+ end
17
+
14
18
  # Direct URL to the record.
15
19
  attr :url <=> '@url', Coercible::String.optional, :read_only
16
20
 
@@ -205,5 +209,24 @@ module Fortnox
205
209
 
206
210
  # WWW Website URL
207
211
  attr :www <=> 'WWW', Sized::String[128]
212
+
213
+ private
214
+
215
+ # ELECTRONICINVOICE is only possible to set in the Fortnox UI.
216
+ # Raise if a consumer tries to set it from this gem.
217
+ def validate_electronic_invoice_delivery_type!(type)
218
+ return unless type == DefaultInvoiceDeliveryTypeValues['ELECTRONICINVOICE']
219
+
220
+ set_by_consumer = meta.new? || __changes__.key?(:default_delivery_types)
221
+
222
+ return unless set_by_consumer
223
+
224
+ raise Fortnox::ConstraintError.new(
225
+ :default_delivery_types,
226
+ 'ELECTRONICINVOICE',
227
+ "Customer#default_delivery_types.invoice cannot be changed to 'ELECTRONICINVOICE' " \
228
+ 'via the API, you can only do that change from within Fortnox.'
229
+ )
230
+ end
208
231
  end
209
232
  end
@@ -3,13 +3,13 @@
3
3
  module Fortnox
4
4
  module Structs
5
5
  class DefaultDeliveryTypes < Fortnox::Struct
6
- # Default delivery type for invoices. Can be PRINT EMAIL or PRINTSERVICE.
7
- attribute? :invoice, Types::DefaultDeliveryTypeValues
6
+ # Default delivery type for invoices.
7
+ attribute? :invoice, Types::DefaultInvoiceDeliveryTypeValues
8
8
 
9
- # Default delivery type for orders. Can be PRINT EMAIL or PRINTSERVICE.
9
+ # Default delivery type for orders.
10
10
  attribute? :order, Types::DefaultDeliveryTypeValues
11
11
 
12
- # Default delivery type for offers. Can be PRINT EMAIL or PRINTSERVICE.
12
+ # Default delivery type for offers.
13
13
  attribute? :offer, Types::DefaultDeliveryTypeValues
14
14
  end
15
15
  end
data/lib/fortnox/types.rb CHANGED
@@ -66,6 +66,10 @@ module Fortnox
66
66
  'PRINT', 'EMAIL', 'PRINTSERVICE'
67
67
  )
68
68
 
69
+ DefaultInvoiceDeliveryTypeValues = Types::Strict::String.enum(
70
+ 'PRINT', 'EMAIL', 'PRINTSERVICE', 'ELECTRONICINVOICE'
71
+ )
72
+
69
73
  AccountingMethods = Types::Strict::String.enum(
70
74
  '', 'ACCRUAL', 'CASH'
71
75
  )
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Fortnox
4
- VERSION = '1.0.0.rc6'
4
+ VERSION = '1.0.0.rc9'
5
5
  end
data/lib/fortnox.rb CHANGED
@@ -93,6 +93,14 @@ module Fortnox
93
93
 
94
94
  OAUTH_TOKEN_URL = 'https://apps.fortnox.se/oauth-v1/token'
95
95
 
96
+ # Character set allowed in Fortnox text fields, per the official Fortnox docs.
97
+ # Useful for pre-validating strings before sending them to the API.
98
+ # See: https://www.fortnox.se/developer/guides-and-good-to-know/formats-and-encoding
99
+ # (The docs spell Unicode codepoints as \x{NNNN}; Ruby's regex parser uses
100
+ # \u{NNNN} for the same thing, so the three codepoints below are translated.)
101
+ ALLOWED_CHARACTERS_REGEXP =
102
+ %r{\A[\p{L}’\\\u{0308}\u{030a}a-zåäöéáœæøüA-ZÅÄÖÉÁÜŒÆØ0-9 –:.`´,;\^¤#%§£$€¢¥©™°&/()=+\-*_!?²³®½@\u{00a0}\n\r]*\z}
103
+
96
104
  class << self
97
105
  def access_token=(token)
98
106
  Thread.current[Auth::ThreadLocal::THREAD_LOCAL_KEY] = token
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.rc6
4
+ version: 1.0.0.rc9
5
5
  platform: ruby
6
6
  authors:
7
7
  - Jonas Schubert Erlandsson
@@ -11,8 +11,22 @@ authors:
11
11
  autorequire:
12
12
  bindir: bin
13
13
  cert_chain: []
14
- date: 2026-05-18 00:00:00.000000000 Z
14
+ date: 2026-05-20 00:00:00.000000000 Z
15
15
  dependencies:
16
+ - !ruby/object:Gem::Dependency
17
+ name: base64
18
+ requirement: !ruby/object:Gem::Requirement
19
+ requirements:
20
+ - - ">="
21
+ - !ruby/object:Gem::Version
22
+ version: '0'
23
+ type: :runtime
24
+ prerelease: false
25
+ version_requirements: !ruby/object:Gem::Requirement
26
+ requirements:
27
+ - - ">="
28
+ - !ruby/object:Gem::Version
29
+ version: '0'
16
30
  - !ruby/object:Gem::Dependency
17
31
  name: countries
18
32
  requirement: !ruby/object:Gem::Requirement
@@ -47,14 +61,14 @@ dependencies:
47
61
  requirements:
48
62
  - - "~>"
49
63
  - !ruby/object:Gem::Version
50
- version: 1.2.0
64
+ version: 1.3.0
51
65
  type: :runtime
52
66
  prerelease: false
53
67
  version_requirements: !ruby/object:Gem::Requirement
54
68
  requirements:
55
69
  - - "~>"
56
70
  - !ruby/object:Gem::Version
57
- version: 1.2.0
71
+ version: 1.3.0
58
72
  description: Fortnox F3 REST API library, based on rest-easy.
59
73
  email:
60
74
  - info@accodeing.com
@@ -116,14 +130,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
116
130
  requirements:
117
131
  - - ">="
118
132
  - !ruby/object:Gem::Version
119
- version: 3.1.0
133
+ version: 3.2.0
120
134
  required_rubygems_version: !ruby/object:Gem::Requirement
121
135
  requirements:
122
136
  - - ">"
123
137
  - !ruby/object:Gem::Version
124
138
  version: 1.3.1
125
139
  requirements: []
126
- rubygems_version: 3.4.6
140
+ rubygems_version: 3.4.19
127
141
  signing_key:
128
142
  specification_version: 4
129
143
  summary: Fortnox F3 REST API library, based on rest-easy.