fortnox-api 1.0.0.rc12 → 1.0.0.rc13

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: 6a383d4f26222585f14be0a06145f63bb4584f8033e2eae2f91dc22cc513c085
4
- data.tar.gz: 71fe26b84df2f8bb30ddcd990b39742606827b507164d42f8b05594b069f8ad6
3
+ metadata.gz: 695db94ae3937bdcc80a1737e65b730cc58dd6784b79eedf6296490b716ab31a
4
+ data.tar.gz: 3a25be836c8d7251578d71b40364c122673176c89b878a7a8ed21d68c0eb2377
5
5
  SHA512:
6
- metadata.gz: c22875f971a82ee3b75168da1395ef21c75bb04ef4e521d550264435c840b5ea786abcde67d8234eded0a06d9ad4028e1926403daf62a3ed8355ae9adfd9634f
7
- data.tar.gz: '08b942421e226597317d74e1189b3a4aca90fcc5158d8b4f6d27d934a791f2af57f80c9226f406015372286187fd286e43d271fcdb8b2e501380001f55ce43f7'
6
+ metadata.gz: c8fb345adff538fbb9e0d1db6c0d653bebbfa35014ecb0b78e7ee5940c9f8f6f77eab563a2378f2d0827822a44a56fe0bf0fffc06e4c961e84850ae07b83adf9
7
+ data.tar.gz: '05914af9563412c342711d69418462bbd47335ea09aa46d13bf581906fd80a81fea2d1b997dc5a27f61c98b54f705637f502c4b44c3d66fcc78aea8c0368dd5c'
data/CHANGELOG.md CHANGED
@@ -8,6 +8,57 @@ and this project adheres to
8
8
 
9
9
  ## [Unreleased]
10
10
 
11
+ ## [1.0.0.rc13] - 2026-08-04
12
+
13
+ ### Changed
14
+
15
+ - String attributes now normalise `''` to `nil` at the type level. This
16
+ is the mechanism behind the reset fix below: Fortnox silently ignores
17
+ empty strings in update payloads — updating an attribute to `''` kept
18
+ the original value — and a field can only be cleared with an explicit
19
+ `null`. Normalising in the type also changes the read side: unset
20
+ string attributes read as `nil` in the model, never `''`. Fortnox
21
+ spells "unset" as `""` or `null` depending on endpoint and record
22
+ history, so the model value was previously unpredictable. Consumers
23
+ comparing against `''` must switch to `nil` checks (or call `.to_s`
24
+ if they prefer empty strings). On create, an attribute set to `''`
25
+ is now omitted from the POST body entirely (new records strip
26
+ `nil`s), leaving the field to Fortnox's default — previously `""`
27
+ was sent, which Fortnox ignores, with the same end result.
28
+ Enum-typed attributes whose value set
29
+ includes `''` (`payment_way`, `accounting_method`, `invoice_type`,
30
+ `tax_reduction_type`, `delivery_state`) are exempt: there `''` is a
31
+ real Fortnox value, not an unset spelling. For required string attributes
32
+ (`Customer#name`, `Article#description`, `Unit#code`/`#description`,
33
+ `TermsOfPayment#code`) this means updating to `''` now raises
34
+ `Fortnox::MissingAttributeError` before any request is made —
35
+ previously the empty string was sent and silently ignored by Fortnox
36
+ (clearing a required field is invalid: Fortnox rejects `null` for
37
+ them with a 400 and ignores `""`).
38
+
39
+ ### Fixed
40
+
41
+ - Resetting a string attribute to `''` on update now clears the value in
42
+ Fortnox. Previously the empty string was silently ignored by Fortnox
43
+ and the old value was left intact (reported for `Customer#comments`).
44
+ Since `''` now coerces to `nil` (see above), both `update(attr: '')`
45
+ and `update(attr: nil)` reach Fortnox as `null`, which clears. This
46
+ includes the country attributes on documents (`country_code`,
47
+ `delivery_country`), whose mapper previously turned `nil` back into
48
+ `""` on the wire, so countries could never be cleared at all.
49
+ - Numeric attributes no longer raise `Fortnox::ConstraintError` when
50
+ Fortnox returns `""` for an unset value — blank coerces to `nil`,
51
+ extending the 1.0.0.rc10 `Types::AccountNumber` fix to all integer
52
+ and float attributes (e.g. `Customer#invoice_discount`,
53
+ `Invoice#balance`). Like that bug, this was introduced in 1.0.0.rc1
54
+ and did not exist in 0.x, which coerced `""` to `0`/`0.0`.
55
+ - `#update`, `#serialise` (and `#to_api`, which goes through it) and
56
+ `.new` now raise Fortnox-namespaced errors. A coercion failure like
57
+ `customer.update(email: 'not-an-email')` previously leaked
58
+ `RestEasy::ConstraintError`, which `rescue Fortnox::ConstraintError`
59
+ blocks don't catch — error translation only covered `save`, `parse`,
60
+ `find`, `stub` and the other class-level paths.
61
+
11
62
  ## [1.0.0.rc12] - 2026-06-26
12
63
 
13
64
  ### Fixed
@@ -281,7 +332,8 @@ for the full list of breaking changes.
281
332
  For changes prior to the 1.0 rewrite, see the
282
333
  [0.x changelog](https://github.com/accodeing/fortnox-api/blob/v0.9.2/CHANGELOG.md).
283
334
 
284
- [Unreleased]: https://github.com/accodeing/fortnox-api/compare/v1.0.0.rc12...HEAD
335
+ [Unreleased]: https://github.com/accodeing/fortnox-api/compare/v1.0.0.rc13...HEAD
336
+ [1.0.0.rc13]: https://github.com/accodeing/fortnox-api/compare/v1.0.0.rc12...v1.0.0.rc13
285
337
  [1.0.0.rc12]: https://github.com/accodeing/fortnox-api/compare/v1.0.0.rc11...v1.0.0.rc12
286
338
  [1.0.0.rc11]: https://github.com/accodeing/fortnox-api/compare/v1.0.0.rc10...v1.0.0.rc11
287
339
  [1.0.0.rc10]: https://github.com/accodeing/fortnox-api/compare/v1.0.0.rc9...v1.0.0.rc10
@@ -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 '' if country.nil? || country == ''
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 '' if country_code.nil? || country_code == ''
29
+ return nil if country_code.nil? || country_code == ''
27
30
 
28
31
  return 'Sverige' if country_code == 'SE'
29
32
 
@@ -26,6 +26,27 @@ module Fortnox
26
26
 
27
27
  after_serialise { |data| default_after_serialise(data) }
28
28
 
29
+ # Translate rest-easy errors at the gem boundary so callers only see
30
+ # Fortnox-namespaced exceptions.
31
+ module ErrorTranslation
32
+ private
33
+
34
+ def with_translated_errors
35
+ yield
36
+ rescue RestEasy::ConstraintError => e
37
+ raise Fortnox::ConstraintError.new(e.attribute_name, e.value, e.message)
38
+ rescue RestEasy::MissingAttributeError => e
39
+ raise Fortnox::MissingAttributeError, e.attribute_name
40
+ rescue RestEasy::AttributeError => e
41
+ raise Fortnox::AttributeError, e.message
42
+ rescue RestEasy::RequestError => e
43
+ raise Fortnox::RequestError, e.response || e.message
44
+ end
45
+ end
46
+
47
+ include ErrorTranslation # instance-level methods
48
+ extend ErrorTranslation # class-level methods
49
+
29
50
  class << self
30
51
  attr_reader :registered_resources
31
52
 
@@ -42,6 +63,10 @@ module Fortnox
42
63
  end
43
64
  end
44
65
 
66
+ def new(...)
67
+ with_translated_errors { super }
68
+ end
69
+
45
70
  def stub(**model_data)
46
71
  with_translated_errors { super }
47
72
  end
@@ -94,20 +119,6 @@ module Fortnox
94
119
 
95
120
  private
96
121
 
97
- # Translate rest-easy errors at the gem boundary so callers only see
98
- # Fortnox-namespaced exceptions.
99
- def with_translated_errors
100
- yield
101
- rescue RestEasy::ConstraintError => e
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
109
- end
110
-
111
122
  def extract_pagination(data)
112
123
  return {} unless data.is_a?(Hash) && data.key?('MetaInformation')
113
124
 
@@ -120,6 +131,14 @@ module Fortnox
120
131
  end
121
132
  end
122
133
 
134
+ def update(...)
135
+ with_translated_errors { super }
136
+ end
137
+
138
+ def serialise(...)
139
+ with_translated_errors { super }
140
+ end
141
+
123
142
  private
124
143
 
125
144
  # Shared serialisation tail used by the base after_serialise hook. Exposed
@@ -12,7 +12,7 @@ module Fortnox
12
12
  end
13
13
 
14
14
  # @url Direct URL to the record.
15
- attr :url <=> '@url', Coercible::String.optional, :read_only
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, Coercible::Float.optional, :read_only
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, Coercible::Float.optional, :read_only
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, Coercible::Float.optional, :read_only
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, Coercible::Float.optional, :read_only
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, Coercible::String.optional, :read_only
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, Coercible::String.optional
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, Coercible::String.optional
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', Coercible::Float.optional
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, Coercible::String.optional
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, Coercible::Float.optional
143
+ attr :direct_cost, UnsizedFloat
144
144
 
145
145
  # FreightCost Freight cost of the article.
146
- attr :freight_cost, Coercible::Float.optional
146
+ attr :freight_cost, UnsizedFloat
147
147
 
148
148
  # OtherCost Other cost of the article.
149
- attr :other_cost, Coercible::Float.optional
149
+ attr :other_cost, UnsizedFloat
150
150
 
151
151
  # DefaultStockPoint Default stock point for the article.
152
- attr :default_stock_point, Coercible::String.optional
152
+ attr :default_stock_point, UnsizedString
153
153
 
154
154
  # DefaultStockLocation Default stock location for the article.
155
- attr :default_stock_location, Coercible::String.optional
155
+ attr :default_stock_location, UnsizedString
156
156
 
157
157
  # CommodityCode Commodity code of the article.
158
- attr :commodity_code, Coercible::String.optional, :read_only
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', Coercible::String.optional, :read_only
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, Coercible::String.optional, :read_only
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, Coercible::String.optional
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, Coercible::String.optional, :read_only
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, Coercible::String.optional, :read_only
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, Coercible::String.optional
157
+ attr :price_list, UnsizedString
158
158
 
159
159
  # Project of the customer
160
- attr :project, Coercible::String.optional
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, Coercible::String.optional
169
+ attr :terms_of_delivery, UnsizedString
170
170
 
171
171
  # Terms of payment code
172
- attr :terms_of_payment, Coercible::String.optional
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', Coercible::String.optional
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, Coercible::String.optional, :read_only
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, Coercible::String.optional
199
+ attr :way_of_delivery, UnsizedString
200
200
 
201
201
  # Your reference
202
202
  attr :your_reference, Sized::String[50]
@@ -8,16 +8,16 @@ module Fortnox
8
8
  using RestEasy::Refinements
9
9
 
10
10
  # Url Direct url to the record.
11
- attr :url <=> '@url', Coercible::String.optional, :read_only
11
+ attr :url <=> '@url', UnsizedString, :read_only
12
12
 
13
13
  # @urlTaxReductionList Direct URL to the tax reduction list.
14
- attr :tax_reduction_list_url <=> '@urlTaxReductionList', Coercible::String.optional, :read_only
14
+ attr :tax_reduction_list_url <=> '@urlTaxReductionList', UnsizedString, :read_only
15
15
 
16
16
  # AdministrationFee The document administration fee.
17
- attr :administration_fee, Coercible::Float.optional
17
+ attr :administration_fee, UnsizedFloat
18
18
 
19
19
  # AdministrationFeeVAT VAT of the document administration fee.
20
- attr :administration_fee_vat <=> 'AdministrationFeeVAT', Coercible::Float.optional, :read_only
20
+ attr :administration_fee_vat <=> 'AdministrationFeeVAT', UnsizedFloat, :read_only
21
21
 
22
22
  # Address1 Document address 1.
23
23
  attr :address1, Sized::String[1024]
@@ -26,7 +26,7 @@ module Fortnox
26
26
  attr :address2, Sized::String[1024]
27
27
 
28
28
  # BasisTaxReduction Basis of tax reduction.
29
- attr :basis_tax_reduction, Coercible::Float.optional, :read_only
29
+ attr :basis_tax_reduction, UnsizedFloat, :read_only
30
30
 
31
31
  # Cancelled If the document is cancelled.
32
32
  attr :cancelled, Bool.optional, :read_only, Boolean
@@ -38,25 +38,25 @@ module Fortnox
38
38
  attr :comments, Sized::String[1024]
39
39
 
40
40
  # ContributionPercent Document contribution in percent.
41
- attr :contribution_percent, Coercible::Float.optional, :read_only
41
+ attr :contribution_percent, UnsizedFloat, :read_only
42
42
 
43
43
  # ContributionValue Document contribution in amount.
44
- attr :contribution_value, Coercible::Float.optional, :read_only
44
+ attr :contribution_value, UnsizedFloat, :read_only
45
45
 
46
46
  # Country Country for the document address.
47
47
  attr :country_code <=> 'Country', Sized::String[2], Mappers::CountryCode
48
48
 
49
49
  # CostCenter Code of the cost center.
50
- attr :cost_center, Coercible::String.optional
50
+ attr :cost_center, UnsizedString
51
51
 
52
52
  # Currency Code of the currency.
53
53
  attr :currency, Currencies
54
54
 
55
55
  # CurrencyRate Currency rate used for the document
56
- attr :currency_rate, Coercible::Float.optional
56
+ attr :currency_rate, UnsizedFloat
57
57
 
58
58
  # CurrencyUnit Currency unit used for the document
59
- attr :currency_unit, Coercible::Float.optional
59
+ attr :currency_unit, UnsizedFloat
60
60
 
61
61
  # CustomerName Name of the customer
62
62
  attr :customer_name, Sized::String[1024]
@@ -86,7 +86,7 @@ module Fortnox
86
86
  attr :delivery_zip_code, Sized::String[1024]
87
87
 
88
88
  # DocumentNumber The document number.
89
- key :document_number, Coercible::Integer.optional
89
+ key :document_number, UnsizedInteger
90
90
 
91
91
  # EmailInformation Separate EmailInformation object
92
92
  attr :email_information, Structs::EmailInformation, Mappers::EmailInformation
@@ -101,10 +101,10 @@ module Fortnox
101
101
  attr :freight, Sized::Float[0.0, 99_999_999_999.9]
102
102
 
103
103
  # FreightVAT VAT of the freight cost.
104
- attr :freight_vat <=> 'FreightVAT', Coercible::Float.optional, :read_only
104
+ attr :freight_vat <=> 'FreightVAT', UnsizedFloat, :read_only
105
105
 
106
106
  # Gross Gross value of the document
107
- attr :gross, Coercible::Float.optional, :read_only
107
+ attr :gross, UnsizedFloat, :read_only
108
108
 
109
109
  # HouseWork If there is any row of the document marked "housework".
110
110
  attr :housework <=> 'HouseWork', Bool.optional, :read_only, Boolean
@@ -113,16 +113,16 @@ module Fortnox
113
113
  attr :labels, Strict::Array.of(Types.Instance(Label)), Mappers::LabelReferences
114
114
 
115
115
  # Net Net amount
116
- attr :net, Coercible::Float.optional, :read_only
116
+ attr :net, UnsizedFloat, :read_only
117
117
 
118
118
  # NotCompleted If the document is set as not completed.
119
119
  attr :not_completed, Bool.optional, Boolean
120
120
 
121
121
  # OfferReference Reference to the offer, if one exists.
122
- attr :offer_reference, Coercible::Integer.optional, :read_only
122
+ attr :offer_reference, UnsizedInteger, :read_only
123
123
 
124
124
  # OrganisationNumber Organisation number of the customer for the document.
125
- attr :organisation_number, Coercible::String.optional, :read_only
125
+ attr :organisation_number, UnsizedString, :read_only
126
126
 
127
127
  # OurReference Our reference
128
128
  attr :our_reference, Sized::String[50]
@@ -134,46 +134,46 @@ module Fortnox
134
134
  attr :phone2, Sized::String[1024]
135
135
 
136
136
  # PriceList Code of the price list.
137
- attr :price_list, Coercible::String.optional
137
+ attr :price_list, UnsizedString
138
138
 
139
139
  # PrintTemplate Print template of the document.
140
- attr :print_template, Coercible::String.optional
140
+ attr :print_template, UnsizedString
141
141
 
142
142
  # Project Code of the project.
143
- attr :project, Coercible::String.optional
143
+ attr :project, UnsizedString
144
144
 
145
145
  # Remarks Remarks of the document
146
146
  attr :remarks, Sized::String[1024]
147
147
 
148
148
  # RoundOff Round off amount for the document.
149
- attr :round_off, Coercible::Float.optional, :read_only
149
+ attr :round_off, UnsizedFloat, :read_only
150
150
 
151
151
  # Sent If the document is printed or sent in any way.
152
152
  attr :sent, Bool.optional, :read_only, Boolean
153
153
 
154
154
  # TaxReduction The amount of tax reduction.
155
- attr :tax_reduction, Coercible::Integer.optional, :read_only
155
+ attr :tax_reduction, UnsizedInteger, :read_only
156
156
 
157
157
  # TaxReductionType Type of tax reduction.
158
158
  attr :tax_reduction_type, TaxReductionTypes
159
159
 
160
160
  # TermsOfDelivery Code of the terms of delivery.
161
- attr :terms_of_delivery, Coercible::String.optional
161
+ attr :terms_of_delivery, UnsizedString
162
162
 
163
163
  # TermsOfPayment Code of the terms of payment.
164
- attr :terms_of_payment, Coercible::String.optional
164
+ attr :terms_of_payment, UnsizedString
165
165
 
166
166
  # Total The total amount of the document.
167
- attr :total, Coercible::Float.optional, :read_only
167
+ attr :total, UnsizedFloat, :read_only
168
168
 
169
169
  # TotalVAT The total VAT amount of the document.
170
- attr :total_vat <=> 'TotalVAT', Coercible::Float.optional, :read_only
170
+ attr :total_vat <=> 'TotalVAT', UnsizedFloat, :read_only
171
171
 
172
172
  # VATIncluded If the price of the document is including VAT.
173
173
  attr :vat_included <=> 'VATIncluded', Bool.optional, Boolean
174
174
 
175
175
  # WayOfDelivery Code of the way of delivery.
176
- attr :way_of_delivery, Coercible::String.optional
176
+ attr :way_of_delivery, UnsizedString
177
177
 
178
178
  # YourOrderNumber Your order number
179
179
  attr :your_order_number, Sized::String[75]
@@ -185,16 +185,16 @@ module Fortnox
185
185
  attr :zip_code, Sized::String[1024]
186
186
 
187
187
  # Language Language code.
188
- attr :language, Coercible::String.optional
188
+ attr :language, UnsizedString
189
189
 
190
190
  # OutboundDate Date of outbound delivery.
191
191
  attr :outbound_date, Date.optional, Mappers::Date
192
192
 
193
193
  # TimeBasisReference Reference to time basis.
194
- attr :time_basis_reference, Coercible::Integer.optional, :read_only
194
+ attr :time_basis_reference, UnsizedInteger, :read_only
195
195
 
196
196
  # TotalToPay Total amount to pay.
197
- attr :total_to_pay, Coercible::Float.optional, :read_only
197
+ attr :total_to_pay, UnsizedFloat, :read_only
198
198
 
199
199
  # WarehouseReady If the document is warehouse ready.
200
200
  attr :warehouse_ready, Bool.optional, :read_only, Boolean
@@ -15,7 +15,7 @@ module Fortnox
15
15
  attr :accounting_method, AccountingMethods, :read_only
16
16
 
17
17
  # Balance Balance of the invoice.
18
- attr :balance, Coercible::Float.optional, :read_only
18
+ attr :balance, UnsizedFloat, :read_only
19
19
 
20
20
  # Booked If the invoice is bookkept.
21
21
  attr :booked, Bool.optional, :read_only, Boolean
@@ -24,10 +24,10 @@ module Fortnox
24
24
  attr :credit, Bool.optional, :read_only, Boolean
25
25
 
26
26
  # CreditInvoiceReference Reference to the credit invoice, if one exists.
27
- attr :credit_invoice_reference, Coercible::Integer.optional
27
+ attr :credit_invoice_reference, UnsizedInteger
28
28
 
29
29
  # ContractReference Reference to the contract, if one exists.
30
- attr :contract_reference, Coercible::Integer.optional, :read_only
30
+ attr :contract_reference, UnsizedInteger, :read_only
31
31
 
32
32
  # DueDate Due date of the invoice.
33
33
  attr :due_date, Date.optional, Mappers::Date
@@ -51,10 +51,10 @@ module Fortnox
51
51
  attr :invoice_period_end, Date.optional, :read_only, Mappers::Date
52
52
 
53
53
  # InvoicePeriodReference Reference to the invoice period.
54
- attr :invoice_period_reference, Coercible::String.optional, :read_only
54
+ attr :invoice_period_reference, UnsizedString, :read_only
55
55
 
56
56
  # InvoiceReference Reference to another invoice.
57
- attr :invoice_reference, Coercible::String.optional, :read_only
57
+ attr :invoice_reference, UnsizedString, :read_only
58
58
 
59
59
  # InvoiceRows Separate object
60
60
  attr :invoice_rows, Strict::Array.of(Structs::InvoiceRow), Mappers::StructArray.for(Mappers::InvoiceRow)
@@ -69,24 +69,24 @@ module Fortnox
69
69
  attr :nox_finans, Bool.optional, :read_only, Boolean
70
70
 
71
71
  # OCR OCR number of the invoice.
72
- attr :ocr <=> 'OCR', Coercible::String.optional
72
+ attr :ocr <=> 'OCR', UnsizedString
73
73
 
74
74
  # OrderReference Reference to the order, if one exists.
75
- attr :order_reference, Coercible::Integer.optional, :read_only
75
+ attr :order_reference, UnsizedInteger, :read_only
76
76
 
77
77
  # PaymentWay Payment way of the invoice.
78
78
  attr :payment_way, PaymentWays
79
79
 
80
80
  # Reminders Number of reminders sent to the customer.
81
- attr :reminders, Coercible::Integer.optional, :read_only
81
+ attr :reminders, UnsizedInteger, :read_only
82
82
 
83
83
  # VoucherNumber Voucher number for the invoice.
84
- attr :voucher_number, Coercible::Integer.optional, :read_only
84
+ attr :voucher_number, UnsizedInteger, :read_only
85
85
 
86
86
  # VoucherSeries Voucher series for the invoice.
87
- attr :voucher_series, Coercible::String.optional, :read_only
87
+ attr :voucher_series, UnsizedString, :read_only
88
88
 
89
89
  # VoucherYear Voucher year for the invoice.
90
- attr :voucher_year, Coercible::Integer.optional, :read_only
90
+ attr :voucher_year, UnsizedInteger, :read_only
91
91
  end
92
92
  end
@@ -9,7 +9,7 @@ module Fortnox
9
9
  scope 'settings'
10
10
  end
11
11
 
12
- attr :id, Coercible::Integer.optional, :read_only, :key
12
+ attr :id, UnsizedInteger, :read_only, :key
13
13
  attr :description, Sized::String[25]
14
14
  end
15
15
  end
@@ -15,7 +15,7 @@ module Fortnox
15
15
  attr :copy_remarks, Bool.optional, Boolean
16
16
 
17
17
  # InvoiceReference Reference if an invoice is created from order
18
- attr :invoice_reference, Coercible::Integer.optional, :read_only
18
+ attr :invoice_reference, UnsizedInteger, :read_only
19
19
 
20
20
  # OrderDate Date of order
21
21
  attr :order_date, Date.optional, Mappers::Date
@@ -27,12 +27,12 @@ module Fortnox
27
27
  attr :delivery_state, DeliveryStates
28
28
 
29
29
  # OrderType Type of order.
30
- attr :order_type, Coercible::String.optional
30
+ attr :order_type, UnsizedString
31
31
 
32
32
  # StockPointCode Code of the stock point.
33
- attr :stock_point_code, Coercible::String.optional
33
+ attr :stock_point_code, UnsizedString
34
34
 
35
35
  # StockPointId ID of the stock point.
36
- attr :stock_point_id, Coercible::String.optional
36
+ attr :stock_point_id, UnsizedString
37
37
  end
38
38
  end
@@ -12,7 +12,7 @@ module Fortnox
12
12
  end
13
13
 
14
14
  # @url Direct URL to the record.
15
- attr :url <=> '@url', Coercible::String.optional, :read_only
15
+ attr :url <=> '@url', UnsizedString, :read_only
16
16
 
17
17
  # Comments Comments on project
18
18
  attr :comments, Sized::String[512]
@@ -12,7 +12,7 @@ module Fortnox
12
12
  end
13
13
 
14
14
  # @url Direct URL to the record.
15
- attr :url <=> '@url', Coercible::String.optional, :read_only
15
+ attr :url <=> '@url', UnsizedString, :read_only
16
16
 
17
17
  # Code The code of the term of payment.
18
18
  key :code, Sized::String[25], :required
@@ -12,7 +12,7 @@ module Fortnox
12
12
  end
13
13
 
14
14
  # @url Direct URL to the record.
15
- attr :url <=> '@url', Coercible::String.optional, :read_only
15
+ attr :url <=> '@url', UnsizedString, :read_only
16
16
 
17
17
  # Code The code of the unit.
18
18
  key :code, Sized::String[20], :required
@@ -10,13 +10,13 @@ module Fortnox
10
10
  attribute? :article_number, Types::Sized::String[50]
11
11
 
12
12
  # ContributionPercent Contribution Percent.
13
- attribute? :contribution_percent, Types::Coercible::Float.optional, :read_only
13
+ attribute? :contribution_percent, Types::UnsizedFloat, :read_only
14
14
 
15
15
  # ContributionValue Contribution Value.
16
- attribute? :contribution_value, Types::Coercible::Float.optional, :read_only
16
+ attribute? :contribution_value, Types::UnsizedFloat, :read_only
17
17
 
18
18
  # CostCenter Code of the cost center for the row.
19
- attribute? :cost_center, Types::Coercible::String.optional
19
+ attribute? :cost_center, Types::UnsizedString
20
20
 
21
21
  # DeliveredQuantity Delivered quantity. 14 digits
22
22
  attribute? :delivered_quantity, Types::Sized::Float[-9_999_999_999_999.9, 9_999_999_999_999.9]
@@ -47,16 +47,16 @@ module Fortnox
47
47
  attribute? :price, Types::Sized::Float[-99_999_999_999.9, 99_999_999_999.9]
48
48
 
49
49
  # Project Code of the project for the row.
50
- attribute? :project, Types::Coercible::String.optional
50
+ attribute? :project, Types::UnsizedString
51
51
 
52
52
  # Total Total amount for the row.
53
- attribute? :total, Types::Coercible::Float.optional, :read_only
53
+ attribute? :total, Types::UnsizedFloat, :read_only
54
54
 
55
55
  # Unit Code of the unit for the row.
56
- attribute? :unit, Types::Coercible::String.optional
56
+ attribute? :unit, Types::UnsizedString
57
57
 
58
58
  # VAT VAT percentage of the row.
59
- attribute? :vat, Types::Coercible::Integer.optional
59
+ attribute? :vat, Types::UnsizedInteger
60
60
  end
61
61
  end
62
62
  end
@@ -4,22 +4,22 @@ module Fortnox
4
4
  module Structs
5
5
  class EDIInformation < Fortnox::Struct
6
6
  # EDIGlobalLocationNumber Invoice address GLN for EDI
7
- attribute? :edi_global_location_number, Types::Coercible::String.optional
7
+ attribute? :edi_global_location_number, Types::UnsizedString
8
8
 
9
9
  # EDIGlobalLocationNumberDelivery Delivery address GLN for EDI
10
- attribute? :edi_global_location_number_delivery, Types::Coercible::String.optional
10
+ attribute? :edi_global_location_number_delivery, Types::UnsizedString
11
11
 
12
12
  # EDIInvoiceExtra1 Extra EDI Information
13
- attribute? :edi_invoice_extra1, Types::Coercible::String.optional
13
+ attribute? :edi_invoice_extra1, Types::UnsizedString
14
14
 
15
15
  # EDIInvoiceExtra2 Extra EDI Information
16
- attribute? :edi_invoice_extra2, Types::Coercible::String.optional
16
+ attribute? :edi_invoice_extra2, Types::UnsizedString
17
17
 
18
18
  # EDIOurElectronicReference Our electronic reference for EDI
19
- attribute? :edi_our_electronic_reference, Types::Coercible::String.optional
19
+ attribute? :edi_our_electronic_reference, Types::UnsizedString
20
20
 
21
21
  # EDIYourElectronicReference Your electronic reference for EDI
22
- attribute? :edi_your_electronic_reference, Types::Coercible::String.optional
22
+ attribute? :edi_your_electronic_reference, Types::UnsizedString
23
23
  end
24
24
  end
25
25
  end
@@ -4,10 +4,10 @@ module Fortnox
4
4
  module Structs
5
5
  class InvoiceRow < DocumentRow
6
6
  # PriceExcludingVAT Price per unit excluding VAT.
7
- attribute? :price_excluding_vat, Types::Coercible::Float.optional, :read_only
7
+ attribute? :price_excluding_vat, Types::UnsizedFloat, :read_only
8
8
 
9
9
  # TotalExcludingVAT Total amount for the row excluding VAT.
10
- attribute? :total_excluding_vat, Types::Coercible::Float.optional, :read_only
10
+ attribute? :total_excluding_vat, Types::UnsizedFloat, :read_only
11
11
  end
12
12
  end
13
13
  end
data/lib/fortnox/types.rb CHANGED
@@ -94,35 +94,65 @@ module Fortnox
94
94
  '', 'none', 'rot', 'rut', 'green'
95
95
  )
96
96
 
97
+ # Fortnox silently ignores "" in update payloads — updating an
98
+ # attribute to '' keeps the original value; only an explicit null
99
+ # clears it. On reads, Fortnox spells "unset" as "" or null depending
100
+ # on endpoint and record history. Normalise at the type boundary:
101
+ # blank coerces to nil on parse and update alike, so "no value" is
102
+ # always nil in the model and always null on the wire. (Enum types
103
+ # that include '' as a valid member are exempt on purpose.)
104
+ BLANK_TO_NIL = ->(value) { value == '' ? nil : value }
105
+
97
106
  # Fortnox sometimes returns "" for unset attributes using AccountNumber as type.
98
- # Without this constructor, "" falls through to Coercible::Integer and
107
+ # Without the blank normalisation, "" falls through to Coercible::Integer and
99
108
  # `Integer("")` raises, surfacing as Fortnox::ConstraintError.
100
109
  AccountNumber = Coercible::Integer
101
110
  .constrained(gteq: 0, lteq: 9999)
102
111
  .optional
103
- .constructor { |v| v == '' ? nil : v }
112
+ .constructor(BLANK_TO_NIL)
104
113
 
105
114
  Email = Strict::String
106
- .constrained(max_size: 1024, format: /\A\z|\A[[[:alnum:]]+-_.]+@[[[:alnum:]]+-_.]+\.[a-z]+\z/i)
115
+ .constrained(max_size: 1024, format: /\A[[[:alnum:]]+-_.]+@[[[:alnum:]]+-_.]+\.[a-z]+\z/i)
107
116
  .optional
108
- .constructor { |v| v&.to_s&.downcase }
117
+ .constructor { |value| BLANK_TO_NIL[value]&.to_s&.downcase }
118
+
119
+ UnsizedString = Types::Coercible::String
120
+ .optional
121
+ .constructor(BLANK_TO_NIL)
122
+
123
+ UnsizedInteger = Types::Coercible::Integer
124
+ .optional
125
+ .constructor(BLANK_TO_NIL)
126
+
127
+ UnsizedFloat = Types::Coercible::Float
128
+ .optional
129
+ .constructor(BLANK_TO_NIL)
109
130
 
110
131
  module Sized
111
132
  module String
112
133
  def self.[](size)
113
- Types::Coercible::String.constrained(max_size: size).optional
134
+ Types::Coercible::String
135
+ .constrained(max_size: size)
136
+ .optional
137
+ .constructor(BLANK_TO_NIL)
114
138
  end
115
139
  end
116
140
 
117
141
  module Integer
118
142
  def self.[](low, high)
119
- Types::Coercible::Integer.constrained(gteq: low, lteq: high).optional
143
+ Types::Coercible::Integer
144
+ .constrained(gteq: low, lteq: high)
145
+ .optional
146
+ .constructor(BLANK_TO_NIL)
120
147
  end
121
148
  end
122
149
 
123
150
  module Float
124
151
  def self.[](low, high)
125
- Types::Coercible::Float.constrained(gteq: low, lteq: high).optional
152
+ Types::Coercible::Float
153
+ .constrained(gteq: low, lteq: high)
154
+ .optional
155
+ .constructor(BLANK_TO_NIL)
126
156
  end
127
157
  end
128
158
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Fortnox
4
- VERSION = '1.0.0.rc12'
4
+ VERSION = '1.0.0.rc13'
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.rc12
4
+ version: 1.0.0.rc13
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-06-26 00:00:00.000000000 Z
14
+ date: 2026-08-04 00:00:00.000000000 Z
15
15
  dependencies:
16
16
  - !ruby/object:Gem::Dependency
17
17
  name: base64