fortnox-api 1.0.0.rc12 → 1.0.0.rc14
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 +120 -1
- data/README.md +52 -0
- data/fortnox.gemspec +1 -1
- data/lib/fortnox/attribute_keys.rb +37 -0
- data/lib/fortnox/collection.rb +1 -0
- data/lib/fortnox/error_translation.rb +22 -0
- data/lib/fortnox/mappers/country_code.rb +5 -2
- data/lib/fortnox/mappers/struct.rb +5 -1
- data/lib/fortnox/rails.rb +47 -0
- data/lib/fortnox/resource.rb +26 -13
- data/lib/fortnox/resources/article.rb +16 -16
- data/lib/fortnox/resources/customer.rb +12 -12
- data/lib/fortnox/resources/document.rb +29 -29
- data/lib/fortnox/resources/invoice.rb +11 -11
- data/lib/fortnox/resources/label.rb +1 -1
- data/lib/fortnox/resources/order.rb +4 -4
- data/lib/fortnox/resources/project.rb +1 -1
- data/lib/fortnox/resources/terms_of_payment.rb +1 -1
- data/lib/fortnox/resources/unit.rb +1 -1
- data/lib/fortnox/serialisation.rb +68 -0
- data/lib/fortnox/struct.rb +88 -0
- data/lib/fortnox/structs/document_row.rb +8 -8
- data/lib/fortnox/structs/edi_information.rb +6 -6
- data/lib/fortnox/structs/invoice_row.rb +2 -2
- data/lib/fortnox/types.rb +44 -8
- data/lib/fortnox/version.rb +1 -1
- data/lib/fortnox.rb +18 -0
- metadata +10 -6
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 1c59313b416138bb0a0544e45c6b2e4166937a184cf95112247a5d45f5a44418
|
|
4
|
+
data.tar.gz: 25d19b69aed753b8099edafcab818c0b6b64ec989c1b2deb57d819b4aae3f69d
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 8f8d49aac36c3a995c580b3776b09ef7ac88047ae01a8dc3435c4287dd12bd68681e79668815853a38d5318e3edbea156381b4298dbf398181fba52907bfdde4
|
|
7
|
+
data.tar.gz: 8066b72c5242c29bef520f44cce65b30e0a12b7d19318972daa887c0da181253fe95bbb9aee86fb532fb5d036148cf0c9f14424fa3a888357d43f102b5b311ed
|
data/CHANGELOG.md
CHANGED
|
@@ -8,6 +8,123 @@ and this project adheres to
|
|
|
8
8
|
|
|
9
9
|
## [Unreleased]
|
|
10
10
|
|
|
11
|
+
## [1.0.0.rc14] - 2026-08-11
|
|
12
|
+
|
|
13
|
+
### Changed
|
|
14
|
+
|
|
15
|
+
- The `base64` runtime dependency is now bounded to `~> 0.2`, matching what
|
|
16
|
+
`rest-easy` already requires. It was declared open-ended when it was added
|
|
17
|
+
in 1.0.0.rc7, so a future `base64` 1.0 with a breaking change would have
|
|
18
|
+
been resolved into consumers' bundles unannounced.
|
|
19
|
+
|
|
20
|
+
### Added
|
|
21
|
+
|
|
22
|
+
- **Breaking** `Fortnox::UnknownAttributeError`, raised when `new`, `stub` or
|
|
23
|
+
`update` is passed an attribute the resource or struct doesn't declare.
|
|
24
|
+
Previously the attribute was silently discarded: the request went out
|
|
25
|
+
without it and Fortnox accepted the result, so a typo cost a field with no
|
|
26
|
+
indication anything was wrong. Worst on nested rows, where
|
|
27
|
+
`stub(order_rows: [{ artcile_number: '1' }])` sent `"OrderRows":[{}]` and
|
|
28
|
+
created an order with empty rows. Subclasses `Fortnox::AttributeError` and
|
|
29
|
+
carries `.attribute_names` (all of them) and `.attribute_name` (the first).
|
|
30
|
+
Consumers passing a wider hash than the resource declares must slice it
|
|
31
|
+
first. Parsing an API response is unaffected and stays tolerant of fields
|
|
32
|
+
the gem doesn't declare — Fortnox adds them over time, and a response must
|
|
33
|
+
not fail to parse because of one.
|
|
34
|
+
- Attribute hashes now accept string keys as well as symbols, on `new`,
|
|
35
|
+
`stub`, `update` and struct constructors. Previously a string key matched
|
|
36
|
+
nothing and was dropped, which is how a Rails controller passing `params`
|
|
37
|
+
through produced records missing every field.
|
|
38
|
+
- Optional Rails integration, `require 'fortnox/rails'`. Defines `as_json` on
|
|
39
|
+
resources, collections and nested structs so `render json:` works when they
|
|
40
|
+
are nested inside another structure — ActiveSupport walks nested objects
|
|
41
|
+
with `as_json`, and without a definition falls through to `Object#as_json`,
|
|
42
|
+
which serialises instance variables and leaks `api_data`,
|
|
43
|
+
`model_attributes`, `changes` and `meta` into response bodies. The file is
|
|
44
|
+
not loaded with the rest of the gem and ActiveSupport is not a runtime
|
|
45
|
+
dependency; requiring it is opt-in for apps that already have Rails.
|
|
46
|
+
|
|
47
|
+
### Fixed
|
|
48
|
+
|
|
49
|
+
- `to_json` on a resource holding nested structs no longer renders them
|
|
50
|
+
through `to_s`. An invoice or order with rows serialised them as
|
|
51
|
+
`"#<Fortnox::Structs::InvoiceRow:0x…>"`. `Fortnox::Struct#to_json` and
|
|
52
|
+
`Fortnox::Collection#to_json` are defined for the same reason — both
|
|
53
|
+
previously fell back to the default `Object#to_json`, producing an
|
|
54
|
+
inspect string rather than JSON. All three render the model representation
|
|
55
|
+
(snake_case attribute names); `to_api` still produces the Fortnox wire
|
|
56
|
+
format. This affects all consumers, not only Rails apps.
|
|
57
|
+
- Nested structs (`Fortnox::Structs::*`) now coerce boolean attributes from
|
|
58
|
+
the string spellings params arrive as (`'true'`, `'false'`, `'yes'`,
|
|
59
|
+
`'no'`, `'1'`, `'0'`, `'on'`, `'off'`), matching what resource attributes
|
|
60
|
+
have always accepted. Previously the struct-level type was strict and
|
|
61
|
+
rejected every string, so `Order.stub(order_rows: [{ housework: 'true' }])`
|
|
62
|
+
failed while the equivalent value on a resource attribute coerced cleanly
|
|
63
|
+
— callers passing Rails controller params had to cast booleans by hand
|
|
64
|
+
before building rows. Affects `DocumentRow#housework` and the `InvoiceRow`
|
|
65
|
+
and `OrderRow` subclasses. This asymmetry was introduced in 1.0.0.rc1 and
|
|
66
|
+
did not exist in 0.x.
|
|
67
|
+
- Struct construction now raises Fortnox-namespaced errors. A bad value in a
|
|
68
|
+
nested struct — reached directly via `Fortnox::Structs::OrderRow.new` or
|
|
69
|
+
indirectly via `stub`/`update` with a nested hash — previously raised
|
|
70
|
+
`Dry::Struct::Error`, which is a `TypeError` outside the `Fortnox::Error`
|
|
71
|
+
hierarchy, so `rescue Fortnox::AttributeError` blocks didn't catch it. It
|
|
72
|
+
now raises `Fortnox::ConstraintError` carrying `.attribute_name` and
|
|
73
|
+
`.value`, with the same message format as the resource-level error. This
|
|
74
|
+
extends the error translation added in 1.0.0.rc13, which covered resources
|
|
75
|
+
but not structs.
|
|
76
|
+
|
|
77
|
+
## [1.0.0.rc13] - 2026-08-04
|
|
78
|
+
|
|
79
|
+
### Changed
|
|
80
|
+
|
|
81
|
+
- **Breaking** String attributes now normalise `''` to `nil` at the type level. This
|
|
82
|
+
is the mechanism behind the reset fix below: Fortnox silently ignores
|
|
83
|
+
empty strings in update payloads — updating an attribute to `''` kept
|
|
84
|
+
the original value — and a field can only be cleared with an explicit
|
|
85
|
+
`null`. Normalising in the type also changes the read side: unset
|
|
86
|
+
string attributes read as `nil` in the model, never `''`. Fortnox
|
|
87
|
+
spells "unset" as `""` or `null` depending on endpoint and record
|
|
88
|
+
history, so the model value was previously unpredictable. Consumers
|
|
89
|
+
comparing against `''` must switch to `nil` checks (or call `.to_s`
|
|
90
|
+
if they prefer empty strings). On create, an attribute set to `''`
|
|
91
|
+
is now omitted from the POST body entirely (new records strip
|
|
92
|
+
`nil`s), leaving the field to Fortnox's default — previously `""`
|
|
93
|
+
was sent, which Fortnox ignores, with the same end result.
|
|
94
|
+
Enum-typed attributes whose value set
|
|
95
|
+
includes `''` (`payment_way`, `accounting_method`, `invoice_type`,
|
|
96
|
+
`tax_reduction_type`, `delivery_state`) are exempt: there `''` is a
|
|
97
|
+
real Fortnox value, not an unset spelling. For required string attributes
|
|
98
|
+
(`Customer#name`, `Article#description`, `Unit#code`/`#description`,
|
|
99
|
+
`TermsOfPayment#code`) this means updating to `''` now raises
|
|
100
|
+
`Fortnox::MissingAttributeError` before any request is made —
|
|
101
|
+
previously the empty string was sent and silently ignored by Fortnox
|
|
102
|
+
(clearing a required field is invalid: Fortnox rejects `null` for
|
|
103
|
+
them with a 400 and ignores `""`).
|
|
104
|
+
|
|
105
|
+
### Fixed
|
|
106
|
+
|
|
107
|
+
- Resetting a string attribute to `''` on update now clears the value in
|
|
108
|
+
Fortnox. Previously the empty string was silently ignored by Fortnox
|
|
109
|
+
and the old value was left intact (reported for `Customer#comments`).
|
|
110
|
+
Since `''` now coerces to `nil` (see above), both `update(attr: '')`
|
|
111
|
+
and `update(attr: nil)` reach Fortnox as `null`, which clears. This
|
|
112
|
+
includes the country attributes on documents (`country_code`,
|
|
113
|
+
`delivery_country`), whose mapper previously turned `nil` back into
|
|
114
|
+
`""` on the wire, so countries could never be cleared at all.
|
|
115
|
+
- Numeric attributes no longer raise `Fortnox::ConstraintError` when
|
|
116
|
+
Fortnox returns `""` for an unset value — blank coerces to `nil`,
|
|
117
|
+
extending the 1.0.0.rc10 `Types::AccountNumber` fix to all integer
|
|
118
|
+
and float attributes (e.g. `Customer#invoice_discount`,
|
|
119
|
+
`Invoice#balance`). Like that bug, this was introduced in 1.0.0.rc1
|
|
120
|
+
and did not exist in 0.x, which coerced `""` to `0`/`0.0`.
|
|
121
|
+
- `#update`, `#serialise` (and `#to_api`, which goes through it) and
|
|
122
|
+
`.new` now raise Fortnox-namespaced errors. A coercion failure like
|
|
123
|
+
`customer.update(email: 'not-an-email')` previously leaked
|
|
124
|
+
`RestEasy::ConstraintError`, which `rescue Fortnox::ConstraintError`
|
|
125
|
+
blocks don't catch — error translation only covered `save`, `parse`,
|
|
126
|
+
`find`, `stub` and the other class-level paths.
|
|
127
|
+
|
|
11
128
|
## [1.0.0.rc12] - 2026-06-26
|
|
12
129
|
|
|
13
130
|
### Fixed
|
|
@@ -281,7 +398,9 @@ for the full list of breaking changes.
|
|
|
281
398
|
For changes prior to the 1.0 rewrite, see the
|
|
282
399
|
[0.x changelog](https://github.com/accodeing/fortnox-api/blob/v0.9.2/CHANGELOG.md).
|
|
283
400
|
|
|
284
|
-
[Unreleased]: https://github.com/accodeing/fortnox-api/compare/v1.0.0.
|
|
401
|
+
[Unreleased]: https://github.com/accodeing/fortnox-api/compare/v1.0.0.rc14...HEAD
|
|
402
|
+
[1.0.0.rc14]: https://github.com/accodeing/fortnox-api/compare/v1.0.0.rc13...v1.0.0.rc14
|
|
403
|
+
[1.0.0.rc13]: https://github.com/accodeing/fortnox-api/compare/v1.0.0.rc12...v1.0.0.rc13
|
|
285
404
|
[1.0.0.rc12]: https://github.com/accodeing/fortnox-api/compare/v1.0.0.rc11...v1.0.0.rc12
|
|
286
405
|
[1.0.0.rc11]: https://github.com/accodeing/fortnox-api/compare/v1.0.0.rc10...v1.0.0.rc11
|
|
287
406
|
[1.0.0.rc10]: https://github.com/accodeing/fortnox-api/compare/v1.0.0.rc9...v1.0.0.rc10
|
data/README.md
CHANGED
|
@@ -397,6 +397,58 @@ hitting some other schema-drift issue — please open an issue. There's a
|
|
|
397
397
|
maintainer-only knob to surface those warnings; see the
|
|
398
398
|
[Developer readme](DEVELOPER_README.md) for details.
|
|
399
399
|
|
|
400
|
+
### Rails
|
|
401
|
+
|
|
402
|
+
Two things are worth setting up before you render or receive Fortnox data in
|
|
403
|
+
a Rails app.
|
|
404
|
+
|
|
405
|
+
**Rendering.** Require the Rails integration once, in an initializer:
|
|
406
|
+
|
|
407
|
+
```ruby
|
|
408
|
+
# config/initializers/fortnox.rb
|
|
409
|
+
require 'fortnox/rails'
|
|
410
|
+
```
|
|
411
|
+
|
|
412
|
+
Without it, `render json:` only works for a resource passed on its own.
|
|
413
|
+
Rendering one *inside* another structure walks it with `as_json`, which the
|
|
414
|
+
gem's types don't define outside this file, so ActiveSupport falls back to
|
|
415
|
+
serialising instance variables and your response body gets the gem's
|
|
416
|
+
internals:
|
|
417
|
+
|
|
418
|
+
```ruby
|
|
419
|
+
# Without fortnox/rails
|
|
420
|
+
render json: { invoices: [Fortnox::Invoice.find(1)] }
|
|
421
|
+
# => {"invoices":[{"api_data":{…},"model_attributes":{…},"changes":[…],"meta":{…}}]}
|
|
422
|
+
|
|
423
|
+
# With it
|
|
424
|
+
# => {"invoices":[{"document_number":1,"invoice_rows":[{"article_number":"101"}],…}]}
|
|
425
|
+
```
|
|
426
|
+
|
|
427
|
+
It covers resources, collections, and nested structs, and produces the same
|
|
428
|
+
representation as `to_json` — model attribute names, not the Fortnox wire
|
|
429
|
+
names. Rendering a collection yields its records, as an array would;
|
|
430
|
+
pagination stays on the object (`.total`, `.pages`, `.current_page`) for you
|
|
431
|
+
to render alongside if you want it.
|
|
432
|
+
|
|
433
|
+
The file is not loaded with the rest of the gem and ActiveSupport is not a
|
|
434
|
+
dependency of this gem — requiring it is opt-in, for apps that already have
|
|
435
|
+
Rails.
|
|
436
|
+
|
|
437
|
+
**Receiving params.** Controller params can be passed through as they arrive.
|
|
438
|
+
String keys are accepted, and string values coerce — booleans from
|
|
439
|
+
`'true'`/`'false'`/`'1'`/`'0'`/`'yes'`/`'no'`/`'on'`/`'off'`, numbers from
|
|
440
|
+
their string forms:
|
|
441
|
+
|
|
442
|
+
```ruby
|
|
443
|
+
Fortnox::Order.stub('customer_number' => '1', 'order_rows' => [{ 'article_number' => '101' }])
|
|
444
|
+
```
|
|
445
|
+
|
|
446
|
+
An attribute the resource or struct doesn't declare raises
|
|
447
|
+
`Fortnox::UnknownAttributeError` rather than being dropped, so a typo or a
|
|
448
|
+
stray param surfaces at the call site instead of producing a record that is
|
|
449
|
+
quietly missing a field. Filter params to the attributes you mean to accept,
|
|
450
|
+
as you would for a model.
|
|
451
|
+
|
|
400
452
|
### Gotchas
|
|
401
453
|
|
|
402
454
|
See [docs/gotchas.md](docs/gotchas.md) for known quirks and edge cases in the
|
data/fortnox.gemspec
CHANGED
|
@@ -21,7 +21,7 @@ Gem::Specification.new do |spec|
|
|
|
21
21
|
|
|
22
22
|
spec.required_ruby_version = '>= 3.2.0'
|
|
23
23
|
|
|
24
|
-
spec.add_dependency 'base64'
|
|
24
|
+
spec.add_dependency 'base64', '~> 0.2'
|
|
25
25
|
spec.add_dependency 'countries', '~> 7.1'
|
|
26
26
|
spec.add_dependency 'dry-struct', '~> 1.5'
|
|
27
27
|
spec.add_dependency 'rest-easy', '~> 1.4.0'
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Fortnox
|
|
4
|
+
# Checks the attribute names a caller passes to `new`, `stub` and `update`.
|
|
5
|
+
#
|
|
6
|
+
# Both rest-easy and Dry::Struct build from the attributes they recognise and
|
|
7
|
+
# ignore everything else, so a typo or a string key used to vanish without a
|
|
8
|
+
# word: the request went out missing that field and Fortnox accepted it. The
|
|
9
|
+
# bigger the payload, the easier that is to miss.
|
|
10
|
+
#
|
|
11
|
+
# This only guards data a caller supplies. Parsing an API response stays
|
|
12
|
+
# tolerant of fields the gem doesn't declare — Fortnox adds them over time,
|
|
13
|
+
# and a response must not fail to parse because of one.
|
|
14
|
+
module AttributeKeys
|
|
15
|
+
module_function
|
|
16
|
+
|
|
17
|
+
def check(data, known:, subject:)
|
|
18
|
+
normalised = normalise(data)
|
|
19
|
+
unknown = normalised.keys - known
|
|
20
|
+
raise Fortnox::UnknownAttributeError.new(unknown, subject) unless unknown.empty?
|
|
21
|
+
|
|
22
|
+
normalised
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# Rails hands params through with string keys. Accept them rather than
|
|
26
|
+
# reporting every attribute in the hash as unknown.
|
|
27
|
+
def normalise(data)
|
|
28
|
+
data.to_h { |key, value| [symbolise(key), value] }
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# Left alone if it can't be a symbol, so it falls through to the unknown
|
|
32
|
+
# check and is reported as an attribute error rather than a NoMethodError.
|
|
33
|
+
def symbolise(key)
|
|
34
|
+
key.respond_to?(:to_sym) ? key.to_sym : key
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
data/lib/fortnox/collection.rb
CHANGED
|
@@ -7,6 +7,7 @@ module Fortnox
|
|
|
7
7
|
# the pagination metadata Fortnox returns alongside collection responses.
|
|
8
8
|
class Collection
|
|
9
9
|
include Enumerable
|
|
10
|
+
include Serialisation::CollectionJSON
|
|
10
11
|
extend Forwardable
|
|
11
12
|
|
|
12
13
|
def_delegators :@items, :each, :first, :last, :size, :length, :empty?, :[], :to_a
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Fortnox
|
|
4
|
+
# Translates rest-easy errors at the gem boundary so callers only see
|
|
5
|
+
# Fortnox-namespaced exceptions. Mixed into Fortnox::Resource both ways —
|
|
6
|
+
# `include` for the instance methods, `extend` for the class-level ones.
|
|
7
|
+
module ErrorTranslation
|
|
8
|
+
private
|
|
9
|
+
|
|
10
|
+
def with_translated_errors
|
|
11
|
+
yield
|
|
12
|
+
rescue RestEasy::ConstraintError => e
|
|
13
|
+
raise Fortnox::ConstraintError.new(e.attribute_name, e.value, e.message)
|
|
14
|
+
rescue RestEasy::MissingAttributeError => e
|
|
15
|
+
raise Fortnox::MissingAttributeError, e.attribute_name
|
|
16
|
+
rescue RestEasy::AttributeError => e
|
|
17
|
+
raise Fortnox::AttributeError, e.message
|
|
18
|
+
rescue RestEasy::RequestError => e
|
|
19
|
+
raise Fortnox::RequestError, e.response || e.message
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
@@ -7,8 +7,11 @@ module Fortnox
|
|
|
7
7
|
module CountryCode
|
|
8
8
|
ISO3166.configure { |config| config.locales = [:en, :sv] }
|
|
9
9
|
|
|
10
|
+
# Blank maps to nil in both directions, mirroring the string types:
|
|
11
|
+
# Fortnox spells "unset" country as "" on reads, and on updates only
|
|
12
|
+
# an explicit null clears the field — "" is silently ignored.
|
|
10
13
|
def self.parse(country)
|
|
11
|
-
return
|
|
14
|
+
return nil if country.nil? || country == ''
|
|
12
15
|
|
|
13
16
|
# Fortnox only supports Swedish translation of Sweden
|
|
14
17
|
return 'SE' if country =~ /^s(e$|we|ve)/i
|
|
@@ -23,7 +26,7 @@ module Fortnox
|
|
|
23
26
|
end
|
|
24
27
|
|
|
25
28
|
def self.serialise(country_code)
|
|
26
|
-
return
|
|
29
|
+
return nil if country_code.nil? || country_code == ''
|
|
27
30
|
|
|
28
31
|
return 'Sverige' if country_code == 'SE'
|
|
29
32
|
|
|
@@ -33,7 +33,11 @@ module Fortnox
|
|
|
33
33
|
attributes = data.transform_keys do |api_key|
|
|
34
34
|
api_to_model_map[api_key] || CONVENTION.parse(api_key)
|
|
35
35
|
end
|
|
36
|
-
|
|
36
|
+
# Drop fields the struct doesn't declare. Fortnox adds them over
|
|
37
|
+
# time, and a response must not fail to parse because of one — the
|
|
38
|
+
# strictness on `new` is aimed at what a caller passes, not at what
|
|
39
|
+
# the API returns.
|
|
40
|
+
struct_class.new(attributes.slice(*struct_class.attribute_names))
|
|
37
41
|
end
|
|
38
42
|
|
|
39
43
|
def serialise(struct)
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'fortnox'
|
|
4
|
+
require 'active_support/json'
|
|
5
|
+
|
|
6
|
+
# Optional Rails integration. Not loaded with the rest of the gem — require it
|
|
7
|
+
# explicitly, e.g. from config/initializers/fortnox.rb:
|
|
8
|
+
#
|
|
9
|
+
# require 'fortnox/rails'
|
|
10
|
+
#
|
|
11
|
+
# Rails serialises nested objects by calling `as_json` on them. Anything that
|
|
12
|
+
# doesn't define it falls through to `Object#as_json`, which dumps instance
|
|
13
|
+
# variables — so without this, `render json: { invoices: [invoice] }` puts the
|
|
14
|
+
# gem's internals (`api_data`, `model_attributes`, `changes`, `meta`) in the
|
|
15
|
+
# response body. `to_json` needs no help here; it is correct without Rails.
|
|
16
|
+
module Fortnox
|
|
17
|
+
# Deliberately not named `Rails`: inside `module Fortnox` that constant would
|
|
18
|
+
# shadow the framework's own for every other file in the gem.
|
|
19
|
+
module RailsSerialisation
|
|
20
|
+
# `:only` / `:except` are applied by Fortnox::Serialisation rather than by
|
|
21
|
+
# ActiveSupport, so they accept string and symbol names alike. The inner
|
|
22
|
+
# `as_json` is then called without options — it only needs to stringify
|
|
23
|
+
# keys and recurse into nested structs.
|
|
24
|
+
module ResourceMethods
|
|
25
|
+
# Same representation `to_json` produces: model attribute names.
|
|
26
|
+
def as_json(options = nil)
|
|
27
|
+
Serialisation.filter(model.attributes, options).as_json
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
module StructMethods
|
|
32
|
+
def as_json(options = nil)
|
|
33
|
+
Serialisation.filter(to_h, options).as_json
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
module CollectionMethods
|
|
38
|
+
def as_json(options = nil)
|
|
39
|
+
to_a.map { |item| item.as_json(options) }
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
Fortnox::Resource.include(Fortnox::RailsSerialisation::ResourceMethods)
|
|
46
|
+
Fortnox::Struct.include(Fortnox::RailsSerialisation::StructMethods)
|
|
47
|
+
Fortnox::Collection.include(Fortnox::RailsSerialisation::CollectionMethods)
|
data/lib/fortnox/resource.rb
CHANGED
|
@@ -1,8 +1,12 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module Fortnox
|
|
4
|
+
# TODO: this class is still over the length limit even with error
|
|
5
|
+
# translation extracted — see todo.md.
|
|
6
|
+
# rubocop:disable Metrics/ClassLength
|
|
4
7
|
class Resource < RestEasy::Resource
|
|
5
8
|
include Fortnox::Types
|
|
9
|
+
include Serialisation::ResourceJSON
|
|
6
10
|
|
|
7
11
|
settings do
|
|
8
12
|
setting :instance_wrapper, reader: true
|
|
@@ -26,6 +30,9 @@ module Fortnox
|
|
|
26
30
|
|
|
27
31
|
after_serialise { |data| default_after_serialise(data) }
|
|
28
32
|
|
|
33
|
+
include ErrorTranslation # instance-level methods
|
|
34
|
+
extend ErrorTranslation # class-level methods
|
|
35
|
+
|
|
29
36
|
class << self
|
|
30
37
|
attr_reader :registered_resources
|
|
31
38
|
|
|
@@ -42,8 +49,12 @@ module Fortnox
|
|
|
42
49
|
end
|
|
43
50
|
end
|
|
44
51
|
|
|
52
|
+
def new(model_data = {}, **kwargs)
|
|
53
|
+
with_translated_errors { super(checked_attributes(model_data.merge(kwargs))) }
|
|
54
|
+
end
|
|
55
|
+
|
|
45
56
|
def stub(**model_data)
|
|
46
|
-
with_translated_errors { super }
|
|
57
|
+
with_translated_errors { super(**checked_attributes(model_data)) }
|
|
47
58
|
end
|
|
48
59
|
|
|
49
60
|
def save(instance)
|
|
@@ -94,18 +105,11 @@ module Fortnox
|
|
|
94
105
|
|
|
95
106
|
private
|
|
96
107
|
|
|
97
|
-
#
|
|
98
|
-
#
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
raise Fortnox::ConstraintError.new(e.attribute_name, e.value, e.message)
|
|
103
|
-
rescue RestEasy::MissingAttributeError => e
|
|
104
|
-
raise Fortnox::MissingAttributeError, e.attribute_name
|
|
105
|
-
rescue RestEasy::AttributeError => e
|
|
106
|
-
raise Fortnox::AttributeError, e.message
|
|
107
|
-
rescue RestEasy::RequestError => e
|
|
108
|
-
raise Fortnox::RequestError, e.response || e.message
|
|
108
|
+
# Attribute names a caller may pass. Read-only attributes are included:
|
|
109
|
+
# rest-easy drops them from the payload later, and rejecting them here
|
|
110
|
+
# would break round-tripping a parsed record back through `update`.
|
|
111
|
+
def checked_attributes(data)
|
|
112
|
+
AttributeKeys.check(data, known: all_attribute_definitions.keys, subject: self)
|
|
109
113
|
end
|
|
110
114
|
|
|
111
115
|
def extract_pagination(data)
|
|
@@ -120,6 +124,14 @@ module Fortnox
|
|
|
120
124
|
end
|
|
121
125
|
end
|
|
122
126
|
|
|
127
|
+
def update(changes = {}, **kwargs)
|
|
128
|
+
with_translated_errors { super(self.class.send(:checked_attributes, changes.merge(kwargs))) }
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
def serialise(...)
|
|
132
|
+
with_translated_errors { super }
|
|
133
|
+
end
|
|
134
|
+
|
|
123
135
|
private
|
|
124
136
|
|
|
125
137
|
# Shared serialisation tail used by the base after_serialise hook. Exposed
|
|
@@ -140,4 +152,5 @@ module Fortnox
|
|
|
140
152
|
{ config.instance_wrapper => data }
|
|
141
153
|
end
|
|
142
154
|
end
|
|
155
|
+
# rubocop:enable Metrics/ClassLength
|
|
143
156
|
end
|
|
@@ -12,7 +12,7 @@ module Fortnox
|
|
|
12
12
|
end
|
|
13
13
|
|
|
14
14
|
# @url Direct URL to the record.
|
|
15
|
-
attr :url <=> '@url',
|
|
15
|
+
attr :url <=> '@url', UnsizedString, :read_only
|
|
16
16
|
|
|
17
17
|
# True If the article is active
|
|
18
18
|
attr :active, Bool.optional, Boolean
|
|
@@ -34,7 +34,7 @@ module Fortnox
|
|
|
34
34
|
attr :description, Sized::String[200], :required
|
|
35
35
|
|
|
36
36
|
# DisposableQuantity Disposable quantity of the article.
|
|
37
|
-
attr :disposable_quantity,
|
|
37
|
+
attr :disposable_quantity, UnsizedFloat, :read_only
|
|
38
38
|
|
|
39
39
|
# EAN EAN bar code
|
|
40
40
|
attr :ean <=> 'EAN', Sized::String[30]
|
|
@@ -80,14 +80,14 @@ module Fortnox
|
|
|
80
80
|
attr :quantity_in_stock, Sized::Float[-100_000_000_000_000.0, 99_999_999_999_999.9]
|
|
81
81
|
|
|
82
82
|
# ReservedQuantity Reserved quantity of the article
|
|
83
|
-
attr :reserved_quantity,
|
|
83
|
+
attr :reserved_quantity, UnsizedFloat, :read_only
|
|
84
84
|
|
|
85
85
|
# SalesAccount Account number for the sales account in Sweden.
|
|
86
86
|
# The number must be of an existing account.
|
|
87
87
|
attr :sales_account, Types::AccountNumber
|
|
88
88
|
|
|
89
89
|
# SalesPrice Price of article for its default price list
|
|
90
|
-
attr :sales_price,
|
|
90
|
+
attr :sales_price, UnsizedFloat, :read_only
|
|
91
91
|
|
|
92
92
|
# StockGoods If the article is stock goods
|
|
93
93
|
attr :stock_goods, Bool.optional, Boolean
|
|
@@ -96,27 +96,27 @@ module Fortnox
|
|
|
96
96
|
attr :stock_place, Sized::String[100]
|
|
97
97
|
|
|
98
98
|
# StockValue Value in stock of the article
|
|
99
|
-
attr :stock_value,
|
|
99
|
+
attr :stock_value, UnsizedFloat, :read_only
|
|
100
100
|
|
|
101
101
|
# StockWarning When to start warning for low quantity in stock
|
|
102
102
|
attr :stock_warning, Sized::Float[0.0, 99_999_999_999_999.9]
|
|
103
103
|
|
|
104
104
|
# SupplierName Name of the supplier
|
|
105
|
-
attr :supplier_name,
|
|
105
|
+
attr :supplier_name, UnsizedString, :read_only
|
|
106
106
|
|
|
107
107
|
# SupplierNumber Supplier number for the article.
|
|
108
108
|
# The number must be of an existing supplier.
|
|
109
|
-
attr :supplier_number,
|
|
109
|
+
attr :supplier_number, UnsizedString
|
|
110
110
|
|
|
111
111
|
# Type The type of the article
|
|
112
112
|
attr :type, ArticleTypes
|
|
113
113
|
|
|
114
114
|
# Unit Unit code for the article.
|
|
115
115
|
# The code must be of an existing unit.
|
|
116
|
-
attr :unit,
|
|
116
|
+
attr :unit, UnsizedString
|
|
117
117
|
|
|
118
118
|
# VAT VAT percent, this is predefined by the VAT for the sales account
|
|
119
|
-
attr :vat <=> 'VAT',
|
|
119
|
+
attr :vat <=> 'VAT', UnsizedFloat
|
|
120
120
|
|
|
121
121
|
# WebshopArticle If the article is a webshop article
|
|
122
122
|
attr :webshop_article, Bool.optional, Boolean
|
|
@@ -131,7 +131,7 @@ module Fortnox
|
|
|
131
131
|
attr :expired, Bool.optional, Boolean
|
|
132
132
|
|
|
133
133
|
# CostCalculationMethod Cost calculation method used for the article.
|
|
134
|
-
attr :cost_calculation_method,
|
|
134
|
+
attr :cost_calculation_method, UnsizedString
|
|
135
135
|
|
|
136
136
|
# StockAccount Account number for stock.
|
|
137
137
|
attr :stock_account, Types::AccountNumber
|
|
@@ -140,21 +140,21 @@ module Fortnox
|
|
|
140
140
|
attr :stock_change_account, Types::AccountNumber
|
|
141
141
|
|
|
142
142
|
# DirectCost Direct cost of the article.
|
|
143
|
-
attr :direct_cost,
|
|
143
|
+
attr :direct_cost, UnsizedFloat
|
|
144
144
|
|
|
145
145
|
# FreightCost Freight cost of the article.
|
|
146
|
-
attr :freight_cost,
|
|
146
|
+
attr :freight_cost, UnsizedFloat
|
|
147
147
|
|
|
148
148
|
# OtherCost Other cost of the article.
|
|
149
|
-
attr :other_cost,
|
|
149
|
+
attr :other_cost, UnsizedFloat
|
|
150
150
|
|
|
151
151
|
# DefaultStockPoint Default stock point for the article.
|
|
152
|
-
attr :default_stock_point,
|
|
152
|
+
attr :default_stock_point, UnsizedString
|
|
153
153
|
|
|
154
154
|
# DefaultStockLocation Default stock location for the article.
|
|
155
|
-
attr :default_stock_location,
|
|
155
|
+
attr :default_stock_location, UnsizedString
|
|
156
156
|
|
|
157
157
|
# CommodityCode Commodity code of the article.
|
|
158
|
-
attr :commodity_code,
|
|
158
|
+
attr :commodity_code, UnsizedString, :read_only
|
|
159
159
|
end
|
|
160
160
|
end
|
|
@@ -25,7 +25,7 @@ module Fortnox
|
|
|
25
25
|
end
|
|
26
26
|
|
|
27
27
|
# Direct URL to the record.
|
|
28
|
-
attr :url <=> '@url',
|
|
28
|
+
attr :url <=> '@url', UnsizedString, :read_only
|
|
29
29
|
|
|
30
30
|
# First address of the customer
|
|
31
31
|
attr :address1, Sized::String[1024]
|
|
@@ -37,7 +37,7 @@ module Fortnox
|
|
|
37
37
|
attr :city, Sized::String[1024]
|
|
38
38
|
|
|
39
39
|
# Country of the customer
|
|
40
|
-
attr :country,
|
|
40
|
+
attr :country, UnsizedString, :read_only
|
|
41
41
|
|
|
42
42
|
# Comments
|
|
43
43
|
attr :comments, Sized::String[1024]
|
|
@@ -46,7 +46,7 @@ module Fortnox
|
|
|
46
46
|
attr :currency, Currencies
|
|
47
47
|
|
|
48
48
|
# Cost center of the customer
|
|
49
|
-
attr :cost_center,
|
|
49
|
+
attr :cost_center, UnsizedString
|
|
50
50
|
|
|
51
51
|
# Country code of the customer
|
|
52
52
|
attr :country_code, Sized::String[2]
|
|
@@ -70,7 +70,7 @@ module Fortnox
|
|
|
70
70
|
attr :delivery_city, Sized::String[1024]
|
|
71
71
|
|
|
72
72
|
# Delivery country of the customer
|
|
73
|
-
attr :delivery_country,
|
|
73
|
+
attr :delivery_country, UnsizedString, :read_only
|
|
74
74
|
|
|
75
75
|
# Delivery country code of the customer
|
|
76
76
|
attr :delivery_country_code, Sized::String[2]
|
|
@@ -145,7 +145,7 @@ module Fortnox
|
|
|
145
145
|
attr :our_reference, Sized::String[50]
|
|
146
146
|
|
|
147
147
|
# Phone number of the customer. Only present in collection responses.
|
|
148
|
-
attr :phone,
|
|
148
|
+
attr :phone, UnsizedString, :read_only
|
|
149
149
|
|
|
150
150
|
# First phone number of the customer
|
|
151
151
|
attr :phone1, Sized::String[1024]
|
|
@@ -154,10 +154,10 @@ module Fortnox
|
|
|
154
154
|
attr :phone2, Sized::String[1024]
|
|
155
155
|
|
|
156
156
|
# Price list of the customer
|
|
157
|
-
attr :price_list,
|
|
157
|
+
attr :price_list, UnsizedString
|
|
158
158
|
|
|
159
159
|
# Project of the customer
|
|
160
|
-
attr :project,
|
|
160
|
+
attr :project, UnsizedString
|
|
161
161
|
|
|
162
162
|
# Sales account of the customer
|
|
163
163
|
attr :sales_account, Types::AccountNumber
|
|
@@ -166,16 +166,16 @@ module Fortnox
|
|
|
166
166
|
attr :show_price_vat_included <=> 'ShowPriceVATIncluded', Bool.optional, Boolean
|
|
167
167
|
|
|
168
168
|
# Terms of delivery code
|
|
169
|
-
attr :terms_of_delivery,
|
|
169
|
+
attr :terms_of_delivery, UnsizedString
|
|
170
170
|
|
|
171
171
|
# Terms of payment code
|
|
172
|
-
attr :terms_of_payment,
|
|
172
|
+
attr :terms_of_payment, UnsizedString
|
|
173
173
|
|
|
174
174
|
# Customer type
|
|
175
175
|
attr :type, CustomerTypes
|
|
176
176
|
|
|
177
177
|
# VAT number of the customer
|
|
178
|
-
attr :vat_number <=> 'VATNumber',
|
|
178
|
+
attr :vat_number <=> 'VATNumber', UnsizedString
|
|
179
179
|
|
|
180
180
|
# VAT type of the customer
|
|
181
181
|
attr :vat_type <=> 'VATType', VATTypes
|
|
@@ -187,7 +187,7 @@ module Fortnox
|
|
|
187
187
|
attr :visiting_city, Sized::String[128]
|
|
188
188
|
|
|
189
189
|
# Visit country of the customer
|
|
190
|
-
attr :visiting_country,
|
|
190
|
+
attr :visiting_country, UnsizedString, :read_only
|
|
191
191
|
|
|
192
192
|
# Visiting country code
|
|
193
193
|
attr :visiting_country_code, Sized::String[2]
|
|
@@ -196,7 +196,7 @@ module Fortnox
|
|
|
196
196
|
attr :visiting_zip_code, Sized::String[10]
|
|
197
197
|
|
|
198
198
|
# Way of delivery code
|
|
199
|
-
attr :way_of_delivery,
|
|
199
|
+
attr :way_of_delivery, UnsizedString
|
|
200
200
|
|
|
201
201
|
# Your reference
|
|
202
202
|
attr :your_reference, Sized::String[50]
|