vatsense 0.1.0 → 0.1.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: 41320914ca748d7abab0a4baa5c65b59f1131a9c16a2e0a0ed1f79014f343765
4
- data.tar.gz: 2700b3a5120dbf857f342a585791240977997c9645e569e2e8089f06ec7db6f2
3
+ metadata.gz: a6cbb8615a5f3366f1abe2d7ccd2c4eae2dbcc4ef9b85f72f2ea990e9aafd3d5
4
+ data.tar.gz: 97a498b7db5317fda34e1e4bcd508fc85a0654a1b64f4b1eac5d777b0cb2ab54
5
5
  SHA512:
6
- metadata.gz: 252a750b1486d10ceda68cbf607dff22fa9429ea65c12fceee157f231e3b24c62ebd71fc1b9c13a57b25aa81fa7a90937d61d2dd19d40f539ac23a156b04d948
7
- data.tar.gz: 8701628a0b041007aef2be237944a01332e528f4558dc117b36242ff60c8eecd72e641108be767ffb66a0a97da17af2265dbe58b73ada82ac3a73f13d8f5ea25
6
+ metadata.gz: 4ad0aaec6739477762e8d267919cf07fd3c863577c2394d982d99805804e7231fc318b09406d2e8cdef0d04c2fc432d14d0ab31bd7f267d15b6ae6f7318c7af2
7
+ data.tar.gz: 510b64b88384f3440a3f811588f428f0f8cb808c284ce4f2e419cad596d73a007e733984aa3ac1cd0db659870b96af0a51e967e192bddcf6e80b3c55870c70d4
data/CHANGELOG.md CHANGED
@@ -1,5 +1,27 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.1.1 (2026-04-01)
4
+
5
+ Full Changelog: [v0.1.0...v0.1.1](https://github.com/VAT-Sense/vatsense-ruby/compare/v0.1.0...v0.1.1)
6
+
7
+ ### Bug Fixes
8
+
9
+ * align path encoding with RFC 3986 section 3.3 ([876d20f](https://github.com/VAT-Sense/vatsense-ruby/commit/876d20f7d38dea0d3e1ce1d9320b583833576b84))
10
+ * **internal:** correct multipart form field name encoding ([96c7cdd](https://github.com/VAT-Sense/vatsense-ruby/commit/96c7cdd5632efd1eb3904179312b2d21ac476439))
11
+ * variable name typo ([6328709](https://github.com/VAT-Sense/vatsense-ruby/commit/63287096c06e8cb992946441e932177c07f2f456))
12
+
13
+
14
+ ### Chores
15
+
16
+ * **ci:** skip lint on metadata-only changes ([e4e8c73](https://github.com/VAT-Sense/vatsense-ruby/commit/e4e8c73343b5045afbe01653e23301d9a43d194c))
17
+ * **ci:** support opting out of skipping builds on metadata-only commits ([9db0960](https://github.com/VAT-Sense/vatsense-ruby/commit/9db09605215e8727e05cffeea18b91005424b590))
18
+ * **internal:** update gitignore ([6122de9](https://github.com/VAT-Sense/vatsense-ruby/commit/6122de9f488047ff89c3df016886700ed802c335))
19
+
20
+
21
+ ### Documentation
22
+
23
+ * **readme:** tailor README to VAT Sense use cases ([2b4823e](https://github.com/VAT-Sense/vatsense-ruby/commit/2b4823e2c9bc31eae4c073461defa8b2029f9be0))
24
+
3
25
  ## 0.1.0 (2026-03-18)
4
26
 
5
27
  Full Changelog: [v0.0.1...v0.1.0](https://github.com/VAT-Sense/vatsense-ruby/compare/v0.0.1...v0.1.0)
data/README.md CHANGED
@@ -1,234 +1,204 @@
1
- # Vat Sense Ruby API library
1
+ # VAT Sense Ruby SDK
2
2
 
3
- The Vat Sense Ruby library provides convenient access to the Vat Sense REST API from any Ruby 3.2.0+ application. It ships with comprehensive types & docstrings in Yard, RBS, and RBI – [see below](https://github.com/VAT-Sense/vatsense-ruby#Sorbet) for usage with Sorbet. The standard library's `net/http` is used as the HTTP transport, with connection pooling via the `connection_pool` gem.
4
-
5
- It is generated with [Stainless](https://www.stainless.com/).
6
-
7
- ## Documentation
8
-
9
- Documentation for releases of this gem can be found [on RubyDoc](https://gemdocs.org/gems/vatsense).
10
-
11
- The REST API documentation can be found on [vatsense.com](https://vatsense.com).
3
+ The official Ruby library for the [VAT Sense](https://vatsense.com) REST API. Validate VAT/EORI numbers, look up VAT/GST rates, calculate prices, convert currencies, and generate VAT-compliant invoices.
12
4
 
13
5
  ## Installation
14
6
 
15
- To use this gem, install via Bundler by adding the following to your application's `Gemfile`:
7
+ ```sh
8
+ gem install vatsense
9
+ ```
16
10
 
17
- <!-- x-release-please-start-version -->
11
+ Or add to your Gemfile:
18
12
 
19
13
  ```ruby
20
- gem "vatsense", "~> 0.1.0"
14
+ gem "vatsense"
21
15
  ```
22
16
 
23
- <!-- x-release-please-end -->
17
+ Requires Ruby 3.2.0+.
24
18
 
25
- ## Usage
19
+ ## Quick start
20
+
21
+ Create a client using your API key from the [VAT Sense dashboard](https://vatsense.com/dashboard). The API uses HTTP Basic Auth with `user` as the username and your API key as the password.
26
22
 
27
23
  ```ruby
28
- require "bundler/setup"
29
24
  require "vatsense"
30
25
 
31
- vat_sense = Vatsense::Client.new(
32
- username: ENV["VAT_SENSE_USERNAME"], # This is the default and can be omitted
33
- password: ENV["VAT_SENSE_PASSWORD"] # This is the default and can be omitted
26
+ client = Vatsense::Client.new(
27
+ username: "user",
28
+ password: "your_api_key",
34
29
  )
35
-
36
- rates = vat_sense.rates.list
37
-
38
- puts(rates.code)
39
30
  ```
40
31
 
41
- ### Handling errors
32
+ You can also set the `VAT_SENSE_USERNAME` and `VAT_SENSE_PASSWORD` environment variables and the client will pick them up automatically.
42
33
 
43
- When the library is unable to connect to the API, or if the API returns a non-success status code (i.e., 4xx or 5xx response), a subclass of `Vatsense::Errors::APIError` will be thrown:
34
+ ### Validate a VAT number
44
35
 
45
36
  ```ruby
46
- begin
47
- rate = vat_sense.rates.list
48
- rescue Vatsense::Errors::APIConnectionError => e
49
- puts("The server could not be reached")
50
- puts(e.cause) # an underlying Exception, likely raised within `net/http`
51
- rescue Vatsense::Errors::RateLimitError => e
52
- puts("A 429 status code was received; we should back off a bit.")
53
- rescue Vatsense::Errors::APIStatusError => e
54
- puts("Another non-200-range status code was received")
55
- puts(e.status)
37
+ response = client.validate.check(vat_number: "GB288305674")
38
+
39
+ if response.data.valid
40
+ puts response.data.company.company_name # "BRITISH BROADCASTING CORPORATION"
41
+ puts response.data.company.company_address
42
+ puts response.data.company.country_code # "GB"
56
43
  end
57
44
  ```
58
45
 
59
- Error codes are as follows:
60
-
61
- | Cause | Error Type |
62
- | ---------------- | -------------------------- |
63
- | HTTP 400 | `BadRequestError` |
64
- | HTTP 401 | `AuthenticationError` |
65
- | HTTP 403 | `PermissionDeniedError` |
66
- | HTTP 404 | `NotFoundError` |
67
- | HTTP 409 | `ConflictError` |
68
- | HTTP 422 | `UnprocessableEntityError` |
69
- | HTTP 429 | `RateLimitError` |
70
- | HTTP >= 500 | `InternalServerError` |
71
- | Other HTTP error | `APIStatusError` |
72
- | Timeout | `APITimeoutError` |
73
- | Network error | `APIConnectionError` |
46
+ VAT validation works for the UK, EU, Australia, Norway, Switzerland, South Africa, and Brazil.
74
47
 
75
- ### Retries
76
-
77
- Certain errors will be automatically retried 2 times by default, with a short exponential backoff.
78
-
79
- Connection errors (for example, due to a network connectivity problem), 408 Request Timeout, 409 Conflict, 429 Rate Limit, >=500 Internal errors, and timeouts will all be retried by default.
80
-
81
- You can use the `max_retries` option to configure or disable this:
48
+ ### Validate an EORI number
82
49
 
83
50
  ```ruby
84
- # Configure the default for all requests:
85
- vat_sense = Vatsense::Client.new(
86
- max_retries: 0 # default is 2
87
- )
51
+ response = client.validate.check(eori_number: "GB123456789000")
88
52
 
89
- # Or, configure per-request:
90
- vat_sense.rates.list(request_options: {max_retries: 5})
53
+ if response.data.valid
54
+ puts response.data.company.company_name
55
+ end
91
56
  ```
92
57
 
93
- ### Timeouts
58
+ EORI validation is available for UK and EU numbers only.
59
+
60
+ ### Get a consultation number
94
61
 
95
- By default, requests will time out after 60 seconds. You can use the timeout option to configure or disable this:
62
+ If you need an official consultation number from VIES (EU) or HMRC (UK), provide your own VAT number as the requester:
96
63
 
97
64
  ```ruby
98
- # Configure the default for all requests:
99
- vat_sense = Vatsense::Client.new(
100
- timeout: nil # default is 60
65
+ response = client.validate.check(
66
+ vat_number: "FR12345678901",
67
+ requester_vat_number: "FR98765432101",
101
68
  )
102
69
 
103
- # Or, configure per-request:
104
- vat_sense.rates.list(request_options: {timeout: 5})
70
+ puts response.data.consultation_number
105
71
  ```
106
72
 
107
- On timeout, `Vatsense::Errors::APITimeoutError` is raised.
108
-
109
- Note that requests that time out are retried by default.
73
+ > **Note:** GB requester numbers only work for GB validations, and EU requester numbers only work for EU validations. Cross-region requests are not supported.
110
74
 
111
- ## Advanced concepts
75
+ ### Find the VAT rate for a country
112
76
 
113
- ### BaseModel
114
-
115
- All parameter and response objects inherit from `Vatsense::Internal::Type::BaseModel`, which provides several conveniences, including:
116
-
117
- 1. All fields, including unknown ones, are accessible with `obj[:prop]` syntax, and can be destructured with `obj => {prop: prop}` or pattern-matching syntax.
118
-
119
- 2. Structural equivalence for equality; if two API calls return the same values, comparing the responses with == will return true.
77
+ ```ruby
78
+ rate = client.rates.find(country_code: "DE")
120
79
 
121
- 3. Both instances and the classes themselves can be pretty-printed.
80
+ puts rate.data.country_name # "Germany"
81
+ puts rate.data.tax_rate.rate # 19.0
82
+ puts rate.data.tax_rate.class_ # "standard"
83
+ ```
122
84
 
123
- 4. Helpers such as `#to_h`, `#deep_to_h`, `#to_json`, and `#to_yaml`.
85
+ ### Find a rate for a specific product type
124
86
 
125
- ### Making custom or undocumented requests
87
+ ```ruby
88
+ rate = client.rates.find(country_code: "DE", type: "ebooks")
126
89
 
127
- #### Undocumented properties
90
+ puts rate.data.tax_rate.rate # 7.0
91
+ puts rate.data.tax_rate.class_ # "reduced"
92
+ ```
128
93
 
129
- You can send undocumented parameters to any endpoint, and read undocumented response properties, like so:
94
+ ### Find a rate by IP address
130
95
 
131
- Note: the `extra_` parameters of the same name overrides the documented parameters.
96
+ Useful for determining the correct rate based on your customer's location:
132
97
 
133
98
  ```ruby
134
- rates =
135
- vat_sense.rates.list(
136
- request_options: {
137
- extra_query: {my_query_parameter: value},
138
- extra_body: {my_body_parameter: value},
139
- extra_headers: {"my-header": value}
140
- }
141
- )
142
-
143
- puts(rates[:my_undocumented_property])
144
- ```
145
-
146
- #### Undocumented request params
99
+ rate = client.rates.find(ip_address: "185.86.151.11")
147
100
 
148
- If you want to explicitly send an extra param, you can do so with the `extra_query`, `extra_body`, and `extra_headers` under the `request_options:` parameter when making a request, as seen in the examples above.
149
-
150
- #### Undocumented endpoints
101
+ puts rate.data.country_code # "GB"
102
+ puts rate.data.tax_rate.rate # 20.0
103
+ ```
151
104
 
152
- To make requests to undocumented endpoints while retaining the benefit of auth, retries, and so on, you can make requests using `client.request`, like so:
105
+ ### Calculate a VAT-inclusive price
153
106
 
154
107
  ```ruby
155
- response = client.request(
156
- method: :post,
157
- path: '/undocumented/endpoint',
158
- query: {"dog": "woof"},
159
- headers: {"useful-header": "interesting-value"},
160
- body: {"hello": "world"}
108
+ result = client.rates.calculate_price(
109
+ price: "100.00",
110
+ tax_type: "excl",
111
+ country_code: "FR",
161
112
  )
162
- ```
163
-
164
- ### Concurrency & connection pooling
165
-
166
- The `Vatsense::Client` instances are threadsafe, but are only are fork-safe when there are no in-flight HTTP requests.
167
-
168
- Each instance of `Vatsense::Client` has its own HTTP connection pool with a default size of 99. As such, we recommend instantiating the client once per application in most settings.
169
113
 
170
- When all available connections from the pool are checked out, requests wait for a new connection to become available, with queue time counting towards the request timeout.
171
-
172
- Unless otherwise specified, other classes in the SDK do not have locks protecting their underlying data structure.
114
+ puts result.data.vat_price.price_incl_vat # Price including VAT
115
+ puts result.data.vat_price.price_excl_vat # Price excluding VAT
116
+ puts result.data.vat_price.vat_rate # VAT rate applied
117
+ puts result.data.vat_price.vat # VAT amount
118
+ ```
173
119
 
174
- ## Sorbet
120
+ ### List all VAT rates
175
121
 
176
- This library provides comprehensive [RBI](https://sorbet.org/docs/rbi) definitions, and has no dependency on sorbet-runtime.
122
+ ```ruby
123
+ rates = client.rates.list
177
124
 
178
- You can provide typesafe request parameters like so:
125
+ rates.data.each do |rate|
126
+ puts "#{rate.country_code}: #{rate.country_name}"
127
+ end
179
128
 
180
- ```ruby
181
- vat_sense.rates.list
129
+ # Filter to EU countries only
130
+ eu_rates = client.rates.list(eu: true)
182
131
  ```
183
132
 
184
- Or, equivalently:
133
+ ## Handling errors
185
134
 
186
- ```ruby
187
- # Hashes work, but are not typesafe:
188
- vat_sense.rates.list
135
+ When the API returns an error, the library raises a typed exception:
189
136
 
190
- # You can also splat a full Params class:
191
- params = Vatsense::RateListParams.new
192
- vat_sense.rates.list(**params)
137
+ ```ruby
138
+ begin
139
+ response = client.validate.check(vat_number: "GB288305674")
140
+ rescue Vatsense::Errors::APIConnectionError => e
141
+ # Network issue, could not reach the API
142
+ puts e.message
143
+ rescue Vatsense::Errors::RateLimitError => e
144
+ # 429: Too many requests (300/min general limit, 3/sec for UK validation)
145
+ puts "Rate limited, try again shortly"
146
+ rescue Vatsense::Errors::APIStatusError => e
147
+ # Covers all other HTTP errors
148
+ puts e.status
149
+ puts e.message
150
+ end
193
151
  ```
194
152
 
195
- ### Enums
196
-
197
- Since this library does not depend on `sorbet-runtime`, it cannot provide [`T::Enum`](https://sorbet.org/docs/tenum) instances. Instead, we provide "tagged symbols" instead, which is always a primitive at runtime:
153
+ A `412` error means the upstream validation service (VIES, HMRC, etc.) is temporarily unavailable. These requests do not count against your usage quota.
198
154
 
199
- ```ruby
200
- # :incl
201
- puts(Vatsense::RateCalculatePriceParams::TaxType::INCL)
155
+ | Status Code | Error Type |
156
+ | ----------- | ------------------------------------------- |
157
+ | 400 | `Vatsense::Errors::BadRequestError` |
158
+ | 401 | `Vatsense::Errors::AuthenticationError` |
159
+ | 404 | `Vatsense::Errors::NotFoundError` |
160
+ | 409 | `Vatsense::Errors::ConflictError` |
161
+ | 429 | `Vatsense::Errors::RateLimitError` |
162
+ | >= 500 | `Vatsense::Errors::InternalServerError` |
163
+ | N/A | `Vatsense::Errors::APIConnectionError` |
202
164
 
203
- # Revealed type: `T.all(Vatsense::RateCalculatePriceParams::TaxType, Symbol)`
204
- T.reveal_type(Vatsense::RateCalculatePriceParams::TaxType::INCL)
205
- ```
165
+ ## Retries
206
166
 
207
- Enum parameters have a "relaxed" type, so you can either pass in enum constants or their literal value:
167
+ Failed requests are automatically retried up to 2 times with exponential backoff. This includes connection errors, timeouts, 429, and 5xx responses.
208
168
 
209
169
  ```ruby
210
- # Using the enum constants preserves the tagged type information:
211
- vat_sense.rates.calculate_price(
212
- tax_type: Vatsense::RateCalculatePriceParams::TaxType::INCL,
213
- #
170
+ # Disable retries
171
+ client = Vatsense::Client.new(
172
+ username: "user",
173
+ password: "your_api_key",
174
+ max_retries: 0,
214
175
  )
215
176
 
216
- # Literal values are also permissible:
217
- vat_sense.rates.calculate_price(
218
- tax_type: :incl,
219
- #
177
+ # Or configure per request
178
+ response = client.validate.check(
179
+ vat_number: "GB288305674",
180
+ request_options: { max_retries: 5 },
220
181
  )
221
182
  ```
222
183
 
223
- ## Versioning
184
+ ## Available services
224
185
 
225
- This package follows [SemVer](https://semver.org/spec/v2.0.0.html) conventions. As the library is in initial development and has a major version of `0`, APIs may change at any time.
186
+ | Service | Description |
187
+ | --------------------- | ----------------------------------------------- |
188
+ | `client.validate` | Validate VAT and EORI numbers |
189
+ | `client.rates` | VAT/GST rate lookups, price calculations |
190
+ | `client.countries` | Country data and province lookups |
191
+ | `client.currency` | Exchange rates and currency conversion |
192
+ | `client.invoice` | Create and manage VAT-compliant invoices |
193
+ | `client.usage` | Check your API usage |
226
194
 
227
- This package considers improvements to the (non-runtime) `*.rbi` and `*.rbs` type definitions to be non-breaking changes.
195
+ ## Documentation
228
196
 
229
- ## Requirements
197
+ Full API documentation is available at [vatsense.com/documentation](https://vatsense.com/documentation).
230
198
 
231
- Ruby 3.2.0 or higher.
199
+ ## Versioning
200
+
201
+ This package follows [SemVer](https://semver.org/spec/v2.0.0.html) conventions. As the library is in initial development and has a major version of `0`, APIs may change at any time.
232
202
 
233
203
  ## Contributing
234
204
 
@@ -157,7 +157,7 @@ module Vatsense
157
157
  in Hash | nil => coerced
158
158
  coerced
159
159
  else
160
- message = "Expected a #{Hash} or #{Vatsense::Internal::Type::BaseModel}, got #{data.inspect}"
160
+ message = "Expected a #{Hash} or #{Vatsense::Internal::Type::BaseModel}, got #{input.inspect}"
161
161
  raise ArgumentError.new(message)
162
162
  end
163
163
  end
@@ -237,6 +237,11 @@ module Vatsense
237
237
  end
238
238
  end
239
239
 
240
+ # @type [Regexp]
241
+ #
242
+ # https://www.rfc-editor.org/rfc/rfc3986.html#section-3.3
243
+ RFC_3986_NOT_PCHARS = /[^A-Za-z0-9\-._~!$&'()*+,;=:@]+/
244
+
240
245
  class << self
241
246
  # @api private
242
247
  #
@@ -247,6 +252,15 @@ module Vatsense
247
252
  "#{uri.scheme}://#{uri.host}#{":#{uri.port}" unless uri.port == uri.default_port}"
248
253
  end
249
254
 
255
+ # @api private
256
+ #
257
+ # @param path [String, Integer]
258
+ #
259
+ # @return [String]
260
+ def encode_path(path)
261
+ path.to_s.gsub(Vatsense::Internal::Util::RFC_3986_NOT_PCHARS) { ERB::Util.url_encode(_1) }
262
+ end
263
+
250
264
  # @api private
251
265
  #
252
266
  # @param path [String, Array<String>]
@@ -259,7 +273,7 @@ module Vatsense
259
273
  in []
260
274
  ""
261
275
  in [String => p, *interpolations]
262
- encoded = interpolations.map { ERB::Util.url_encode(_1) }
276
+ encoded = interpolations.map { encode_path(_1) }
263
277
  format(p, *encoded)
264
278
  end
265
279
  end
@@ -571,16 +585,15 @@ module Vatsense
571
585
  y << "Content-Disposition: form-data"
572
586
 
573
587
  unless key.nil?
574
- name = ERB::Util.url_encode(key.to_s)
575
- y << "; name=\"#{name}\""
588
+ y << "; name=\"#{key}\""
576
589
  end
577
590
 
578
591
  case val
579
592
  in Vatsense::FilePart unless val.filename.nil?
580
- filename = ERB::Util.url_encode(val.filename)
593
+ filename = encode_path(val.filename)
581
594
  y << "; filename=\"#{filename}\""
582
595
  in Pathname | IO
583
- filename = ERB::Util.url_encode(::File.basename(val.to_path))
596
+ filename = encode_path(::File.basename(val.to_path))
584
597
  y << "; filename=\"#{filename}\""
585
598
  else
586
599
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Vatsense
4
- VERSION = "0.1.0"
4
+ VERSION = "0.1.1"
5
5
  end
@@ -148,12 +148,20 @@ module Vatsense
148
148
  end
149
149
  end
150
150
 
151
+ # https://www.rfc-editor.org/rfc/rfc3986.html#section-3.3
152
+ RFC_3986_NOT_PCHARS = T.let(/[^A-Za-z0-9\-._~!$&'()*+,;=:@]+/, Regexp)
153
+
151
154
  class << self
152
155
  # @api private
153
156
  sig { params(uri: URI::Generic).returns(String) }
154
157
  def uri_origin(uri)
155
158
  end
156
159
 
160
+ # @api private
161
+ sig { params(path: T.any(String, Integer)).returns(String) }
162
+ def encode_path(path)
163
+ end
164
+
157
165
  # @api private
158
166
  sig { params(path: T.any(String, T::Array[String])).returns(String) }
159
167
  def interpolate_path(path)
@@ -45,8 +45,12 @@ module Vatsense
45
45
  -> top?
46
46
  } -> top?
47
47
 
48
+ RFC_3986_NOT_PCHARS: Regexp
49
+
48
50
  def self?.uri_origin: (URI::Generic uri) -> String
49
51
 
52
+ def self?.encode_path: (String | Integer path) -> String
53
+
50
54
  def self?.interpolate_path: (String | ::Array[String] path) -> String
51
55
 
52
56
  def self?.decode_query: (String? query) -> ::Hash[String, ::Array[String]]
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: vatsense
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 0.1.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Vat Sense
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2026-03-18 00:00:00.000000000 Z
11
+ date: 2026-04-04 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: cgi