cvrvaelger 0.1.0
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 +7 -0
- data/.env.example +7 -0
- data/.rubocop.yml +55 -0
- data/CHANGELOG.md +11 -0
- data/LICENSE.txt +21 -0
- data/README.md +102 -0
- data/cvrvaelger.gemspec +33 -0
- data/lib/cvrvaelger/client.rb +238 -0
- data/lib/cvrvaelger/company.rb +78 -0
- data/lib/cvrvaelger/error.rb +14 -0
- data/lib/cvrvaelger/version.rb +5 -0
- data/lib/cvrvaelger.rb +13 -0
- metadata +59 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: b4c8df3ad4e794628d2f3e5a0010b9a9aa4e35123a64370926196639cb119eb6
|
|
4
|
+
data.tar.gz: 7395f85cc2c4bb3cf458b80d61da01602af672b893f2317e900e833257a64601
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 3623ddc0be170cf6cdbed892a4ea9d18dee2958687945e23982b5271a19e45eb5c33e3c095c3e249177ae442d9c31b103e3f6739f4501b15532efd681c379c96
|
|
7
|
+
data.tar.gz: c622efb3b0d2001065bc756baf0b1bc9be9b746a67875e553b7aa40b4006c682c02f555204095587c241ac7c2f13628ea47442af83f19c4a6150e713db1c85cd
|
data/.env.example
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
# Free Datafordeler IT-system API key (no demo default in the gem).
|
|
2
|
+
# Create at https://datafordeler.dk/ → Administration → IT-system → API key.
|
|
3
|
+
CVRVAELGER_API_KEY=
|
|
4
|
+
# Optional alias:
|
|
5
|
+
# DATAFORDELER_API_KEY=
|
|
6
|
+
# Optional endpoint override (default https://graphql.datafordeler.dk/CVR/v2):
|
|
7
|
+
# CVRVAELGER_BASE_URL=
|
data/.rubocop.yml
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
plugins:
|
|
2
|
+
- rubocop-minitest
|
|
3
|
+
- rubocop-rake
|
|
4
|
+
|
|
5
|
+
AllCops:
|
|
6
|
+
NewCops: enable
|
|
7
|
+
TargetRubyVersion: 3.3
|
|
8
|
+
SuggestExtensions: false
|
|
9
|
+
Exclude:
|
|
10
|
+
- "vendor/**/*"
|
|
11
|
+
- "tmp/**/*"
|
|
12
|
+
- "pkg/**/*"
|
|
13
|
+
|
|
14
|
+
Style/Documentation:
|
|
15
|
+
Enabled: false
|
|
16
|
+
|
|
17
|
+
Style/StringLiterals:
|
|
18
|
+
EnforcedStyle: double_quotes
|
|
19
|
+
|
|
20
|
+
Layout/LineLength:
|
|
21
|
+
Max: 120
|
|
22
|
+
|
|
23
|
+
Metrics/MethodLength:
|
|
24
|
+
Max: 30
|
|
25
|
+
Exclude:
|
|
26
|
+
- "test/**/*"
|
|
27
|
+
|
|
28
|
+
Metrics/ClassLength:
|
|
29
|
+
Max: 200
|
|
30
|
+
Exclude:
|
|
31
|
+
- "test/**/*"
|
|
32
|
+
|
|
33
|
+
Metrics/AbcSize:
|
|
34
|
+
Max: 40
|
|
35
|
+
Exclude:
|
|
36
|
+
- "test/**/*"
|
|
37
|
+
|
|
38
|
+
Metrics/CyclomaticComplexity:
|
|
39
|
+
Max: 12
|
|
40
|
+
Exclude:
|
|
41
|
+
- "test/**/*"
|
|
42
|
+
|
|
43
|
+
Metrics/PerceivedComplexity:
|
|
44
|
+
Max: 12
|
|
45
|
+
Exclude:
|
|
46
|
+
- "test/**/*"
|
|
47
|
+
|
|
48
|
+
Metrics/BlockLength:
|
|
49
|
+
Exclude:
|
|
50
|
+
- "test/**/*"
|
|
51
|
+
- "*.gemspec"
|
|
52
|
+
- "Rakefile"
|
|
53
|
+
|
|
54
|
+
Minitest/MultipleAssertions:
|
|
55
|
+
Max: 10
|
data/CHANGELOG.md
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## 0.1.0
|
|
4
|
+
|
|
5
|
+
- Initial public release: Datafordeler CVR GraphQL v2 client (`lookup` / `search`).
|
|
6
|
+
- Returns company name, light address fields, and status for an 8-digit CVR or
|
|
7
|
+
exact registered name match. Optional `at:` sets GraphQL `virkningstid`.
|
|
8
|
+
- Requires a free Datafordeler API key (`CVRVAELGER_API_KEY`); no demo secret.
|
|
9
|
+
- VCR cassettes for LEGO, Salling Group, empty searches, and the public query
|
|
10
|
+
surface (API keys redacted).
|
|
11
|
+
- RuboCop in CI (`bundle exec rake rubocop`).
|
data/LICENSE.txt
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Rasmus Bergholdt
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
# Cvrvaelger
|
|
2
|
+
|
|
3
|
+
Ruby client for **Det Centrale Virksomhedsregister (CVR)** via
|
|
4
|
+
[Datafordeler GraphQL](https://datafordeler.dk/dataoversigt/det-centrale-virksomhedsregister-cvr/cvr-graphql/).
|
|
5
|
+
|
|
6
|
+
Denmark only — look up an 8-digit CVR or registered company name and return
|
|
7
|
+
picker-shaped fields (name, light address, status).
|
|
8
|
+
|
|
9
|
+
## Install
|
|
10
|
+
|
|
11
|
+
```ruby
|
|
12
|
+
# Gemfile
|
|
13
|
+
gem "cvrvaelger"
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
```sh
|
|
17
|
+
bundle add cvrvaelger
|
|
18
|
+
# or: gem install cvrvaelger
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
Requires Ruby 3.3+. No Rails dependency.
|
|
22
|
+
|
|
23
|
+
## Usage
|
|
24
|
+
|
|
25
|
+
```ruby
|
|
26
|
+
require "cvrvaelger"
|
|
27
|
+
|
|
28
|
+
client = Cvrvaelger::Client.new(
|
|
29
|
+
api_key: ENV.fetch("CVRVAELGER_API_KEY")
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
company = client.lookup("47458714")
|
|
33
|
+
company.cvr # => "47458714"
|
|
34
|
+
company.name # => "LEGO SYSTEM A/S"
|
|
35
|
+
company.address_line # => "Åstvej 1"
|
|
36
|
+
company.postal_code # => "7190"
|
|
37
|
+
company.city # => "Billund"
|
|
38
|
+
company.status # => "aktiv"
|
|
39
|
+
|
|
40
|
+
# Picker: 8 digits → lookup; otherwise exact, case-sensitive name match.
|
|
41
|
+
# Datafordeler string filters are `eq` / `in` only (no contains / fuzzy).
|
|
42
|
+
hits = client.search("LEGO SYSTEM A/S")
|
|
43
|
+
client.search("lego") # => []
|
|
44
|
+
|
|
45
|
+
# Optional bitemporal cut-off (GraphQL `virkningstid`, default: now)
|
|
46
|
+
client.lookup("47458714", at: Time.utc(2020, 1, 1))
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
Environment variables:
|
|
50
|
+
|
|
51
|
+
| Variable | Purpose |
|
|
52
|
+
| --- | --- |
|
|
53
|
+
| `CVRVAELGER_API_KEY` | Datafordeler API key (preferred) |
|
|
54
|
+
| `DATAFORDELER_API_KEY` | Accepted alias |
|
|
55
|
+
| `CVRVAELGER_BASE_URL` | Override GraphQL endpoint (default `https://graphql.datafordeler.dk/CVR/v2`) |
|
|
56
|
+
|
|
57
|
+
## API key
|
|
58
|
+
|
|
59
|
+
There is **no demo/default secret**. Create a free user + IT-system API key in
|
|
60
|
+
[Datafordeler Administration](https://datafordeler.dk/). Company entities do not
|
|
61
|
+
require a special CVR access request; `CVRPerson` is out of scope for this gem.
|
|
62
|
+
|
|
63
|
+
## Testing
|
|
64
|
+
|
|
65
|
+
```sh
|
|
66
|
+
bundle exec rake test
|
|
67
|
+
bundle exec rake rubocop
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
Unit tests stub HTTP via an injected callable. Live examples use
|
|
71
|
+
[VCR](https://github.com/vcr/vcr) cassettes recorded against the real API
|
|
72
|
+
(API keys redacted as `<API_KEY>`). CI uses `record: :none` (fail-closed).
|
|
73
|
+
To refresh cassettes locally:
|
|
74
|
+
|
|
75
|
+
```sh
|
|
76
|
+
VCR_RECORD=all bundle exec rake test TEST=test/client_live_test.rb
|
|
77
|
+
VCR_RECORD=all bundle exec rake test TEST=test/graphql_features_test.rb
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
Never hand-write cassette response bodies.
|
|
81
|
+
|
|
82
|
+
## Attribution
|
|
83
|
+
|
|
84
|
+
Company data via [Datafordeler](https://datafordeler.dk/) / Erhvervsstyrelsen CVR.
|
|
85
|
+
This gem is an independent open-source client and is not affiliated with or
|
|
86
|
+
endorsed by those agencies.
|
|
87
|
+
|
|
88
|
+
## Releasing
|
|
89
|
+
|
|
90
|
+
1. Bump `Cvrvaelger::VERSION` in `lib/cvrvaelger/version.rb` (gemspec reads it).
|
|
91
|
+
2. Update `CHANGELOG.md` for that version.
|
|
92
|
+
3. Commit and push to `main`.
|
|
93
|
+
4. Tag and push: `git tag vX.Y.Z && git push origin vX.Y.Z`
|
|
94
|
+
(or create a GitHub Release for `vX.Y.Z` — that also pushes the tag).
|
|
95
|
+
5. Tag **must** match the gem version (`v0.1.0` ↔ `0.1.0`). The
|
|
96
|
+
[push_gem](.github/workflows/push_gem.yml) workflow verifies this, then
|
|
97
|
+
publishes via [RubyGems Trusted Publishing](https://guides.rubygems.org/trusted-publishing/)
|
|
98
|
+
(`rubygems/release-gem`, OIDC — no `RUBYGEMS_API_KEY` secret).
|
|
99
|
+
|
|
100
|
+
## License
|
|
101
|
+
|
|
102
|
+
MIT. See [LICENSE.txt](LICENSE.txt).
|
data/cvrvaelger.gemspec
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "lib/cvrvaelger/version"
|
|
4
|
+
|
|
5
|
+
Gem::Specification.new do |spec|
|
|
6
|
+
spec.name = "cvrvaelger"
|
|
7
|
+
spec.version = Cvrvaelger::VERSION
|
|
8
|
+
spec.authors = ["Rasmus Bergholdt"]
|
|
9
|
+
spec.email = ["rasmus.bergholdt@gmail.com"]
|
|
10
|
+
|
|
11
|
+
spec.summary = "Ruby client for Danish CVR company lookup via Datafordeler GraphQL."
|
|
12
|
+
spec.description = <<~DESC
|
|
13
|
+
HTTP client for Det Centrale Virksomhedsregister (CVR) on Datafordeler GraphQL:
|
|
14
|
+
verify an 8-digit CVR number or exact registered name and return the company
|
|
15
|
+
name plus light address fields for picker UX. No Rails dependency.
|
|
16
|
+
DESC
|
|
17
|
+
spec.homepage = "https://github.com/bergholdt/cvrvaelger"
|
|
18
|
+
spec.license = "MIT"
|
|
19
|
+
spec.required_ruby_version = ">= 3.3.0"
|
|
20
|
+
|
|
21
|
+
spec.metadata["homepage_uri"] = spec.homepage
|
|
22
|
+
spec.metadata["source_code_uri"] = "https://github.com/bergholdt/cvrvaelger/tree/v#{spec.version}"
|
|
23
|
+
spec.metadata["changelog_uri"] = "https://github.com/bergholdt/cvrvaelger/blob/v#{spec.version}/CHANGELOG.md"
|
|
24
|
+
spec.metadata["bug_tracker_uri"] = "https://github.com/bergholdt/cvrvaelger/issues"
|
|
25
|
+
spec.metadata["rubygems_mfa_required"] = "true"
|
|
26
|
+
|
|
27
|
+
spec.files = Dir.chdir(__dir__) do
|
|
28
|
+
`git ls-files -z`.split("\x0").reject do |f|
|
|
29
|
+
f.start_with?(*%w[bin/ test/ .git .github Gemfile Rakefile AGENTS.md])
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
spec.require_paths = ["lib"]
|
|
33
|
+
end
|
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "net/http"
|
|
4
|
+
require "json"
|
|
5
|
+
require "uri"
|
|
6
|
+
|
|
7
|
+
require_relative "company"
|
|
8
|
+
|
|
9
|
+
module Cvrvaelger
|
|
10
|
+
# HTTP client for Det Centrale Virksomhedsregister via Datafordeler GraphQL.
|
|
11
|
+
#
|
|
12
|
+
# Official endpoint: https://graphql.datafordeler.dk/CVR/v2
|
|
13
|
+
# Auth: free API key from Datafordeler Administration (query param `apiKey`).
|
|
14
|
+
# No demo/default secret — configure CVRVAELGER_API_KEY or pass api_key:.
|
|
15
|
+
class Client
|
|
16
|
+
DEFAULT_BASE_URL = "https://graphql.datafordeler.dk/CVR/v2"
|
|
17
|
+
|
|
18
|
+
# Datafordeler allows one root field per query. Name match is exact (`eq`);
|
|
19
|
+
# there is no contains/fuzzy operator. `virkningstid` selects the bitemporal row.
|
|
20
|
+
VIRKSOMHED_BY_CVR_QUERY = <<~GRAPHQL
|
|
21
|
+
query LookupVirksomhed($cvr: Long!, $at: DafDateTime!) {
|
|
22
|
+
CVR_Virksomhed(first: 1, virkningstid: $at, where: { CVRNummer: { eq: $cvr } }) {
|
|
23
|
+
nodes { id CVRNummer status }
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
GRAPHQL
|
|
27
|
+
|
|
28
|
+
VIRKSOMHED_BY_ID_QUERY = <<~GRAPHQL
|
|
29
|
+
query VirksomhedById($id: String!, $at: DafDateTime!) {
|
|
30
|
+
CVR_Virksomhed(first: 1, virkningstid: $at, where: { id: { eq: $id } }) {
|
|
31
|
+
nodes { id CVRNummer status }
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
GRAPHQL
|
|
35
|
+
|
|
36
|
+
NAVN_BY_ID_QUERY = <<~GRAPHQL
|
|
37
|
+
query NavnById($id: String!, $at: DafDateTime!) {
|
|
38
|
+
CVR_Navn(first: 1, virkningstid: $at, where: { CVREnhedsId: { eq: $id } }) {
|
|
39
|
+
nodes { vaerdi }
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
GRAPHQL
|
|
43
|
+
|
|
44
|
+
NAVN_BY_VALUE_QUERY = <<~GRAPHQL
|
|
45
|
+
query SearchNavn($q: String!, $at: DafDateTime!) {
|
|
46
|
+
CVR_Navn(first: 10, virkningstid: $at, where: { vaerdi: { eq: $q } }) {
|
|
47
|
+
nodes { CVREnhedsId vaerdi }
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
GRAPHQL
|
|
51
|
+
|
|
52
|
+
ADRESSE_BY_ID_QUERY = <<~GRAPHQL
|
|
53
|
+
query AdresseById($id: String!, $at: DafDateTime!) {
|
|
54
|
+
CVR_Adressering(first: 5, virkningstid: $at, where: { CVREnhedsId: { eq: $id } }) {
|
|
55
|
+
nodes {
|
|
56
|
+
AdresseringAnvendelse
|
|
57
|
+
CVRAdresse_vejnavn
|
|
58
|
+
CVRAdresse_husnummerFra
|
|
59
|
+
CVRAdresse_postnummer
|
|
60
|
+
CVRAdresse_postdistrikt
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
GRAPHQL
|
|
65
|
+
|
|
66
|
+
attr_reader :base_url, :api_key
|
|
67
|
+
|
|
68
|
+
def initialize(api_key: nil, base_url: nil, http: nil)
|
|
69
|
+
@api_key = present_string(api_key) || present_string(ENV.fetch("CVRVAELGER_API_KEY", nil)) ||
|
|
70
|
+
present_string(ENV.fetch("DATAFORDELER_API_KEY", nil))
|
|
71
|
+
@base_url = present_string(base_url) || present_string(ENV.fetch("CVRVAELGER_BASE_URL", nil)) || DEFAULT_BASE_URL
|
|
72
|
+
@http = http
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
# Verify an 8-digit CVR and return Company, or nil when not found.
|
|
76
|
+
# `at` is the bitemporal virkningstid (default: now).
|
|
77
|
+
def lookup(cvr, at: Time.now)
|
|
78
|
+
normalized = Company.normalize_cvr(cvr)
|
|
79
|
+
return nil unless present?(normalized)
|
|
80
|
+
|
|
81
|
+
ensure_api_key!
|
|
82
|
+
stamp = format_time(at)
|
|
83
|
+
payload = post_graphql(VIRKSOMHED_BY_CVR_QUERY, cvr: normalized.to_i, at: stamp)
|
|
84
|
+
node = first_node(payload)
|
|
85
|
+
return nil unless present?(node)
|
|
86
|
+
|
|
87
|
+
company_from_virksomhed(node, stamp)
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
# Picker search: 8 digits → lookup; otherwise exact, case-sensitive name match.
|
|
91
|
+
def search(query, at: Time.now)
|
|
92
|
+
q = query.to_s.strip
|
|
93
|
+
return [] if q.length < 2
|
|
94
|
+
|
|
95
|
+
digits = Company.normalize_cvr(q)
|
|
96
|
+
if digits
|
|
97
|
+
company = lookup(digits, at: at)
|
|
98
|
+
return company ? [company] : []
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
ensure_api_key!
|
|
102
|
+
stamp = format_time(at)
|
|
103
|
+
payload = post_graphql(NAVN_BY_VALUE_QUERY, q: q, at: stamp)
|
|
104
|
+
Array(nodes(payload)).filter_map { |node| node["CVREnhedsId"] }.uniq.filter_map do |id|
|
|
105
|
+
company_from_id(id, stamp)
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
private
|
|
110
|
+
|
|
111
|
+
attr_reader :http
|
|
112
|
+
|
|
113
|
+
def ensure_api_key!
|
|
114
|
+
return if present?(api_key)
|
|
115
|
+
|
|
116
|
+
raise ConfigurationError,
|
|
117
|
+
"CVRVAELGER_API_KEY (or DATAFORDELER_API_KEY) is required — " \
|
|
118
|
+
"create a free API key in Datafordeler Administration"
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
def company_from_id(id, stamp)
|
|
122
|
+
payload = post_graphql(VIRKSOMHED_BY_ID_QUERY, id: id, at: stamp)
|
|
123
|
+
node = first_node(payload)
|
|
124
|
+
return nil unless present?(node)
|
|
125
|
+
|
|
126
|
+
company_from_virksomhed(node, stamp)
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
def company_from_virksomhed(node, stamp)
|
|
130
|
+
id = node["id"]
|
|
131
|
+
return nil unless present?(id)
|
|
132
|
+
|
|
133
|
+
name = first_node(post_graphql(NAVN_BY_ID_QUERY, id: id, at: stamp))&.dig("vaerdi")
|
|
134
|
+
address = preferred_address(nodes(post_graphql(ADRESSE_BY_ID_QUERY, id: id, at: stamp)))
|
|
135
|
+
company = Company.from_node(
|
|
136
|
+
"cvrNummer" => format("%08d", node["CVRNummer"].to_i),
|
|
137
|
+
"navn" => name,
|
|
138
|
+
"status" => node["status"],
|
|
139
|
+
"adresse" => address_fields(address)
|
|
140
|
+
)
|
|
141
|
+
return nil unless present?(company.cvr) && present?(company.name)
|
|
142
|
+
|
|
143
|
+
company
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
def preferred_address(list)
|
|
147
|
+
rows = Array(list).grep(Hash)
|
|
148
|
+
rows.find { |row| row["AdresseringAnvendelse"] == "beliggenhedsadresse" } || rows.first
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
def address_fields(address)
|
|
152
|
+
return {} unless address.is_a?(Hash)
|
|
153
|
+
|
|
154
|
+
{
|
|
155
|
+
"vejnavn" => address["CVRAdresse_vejnavn"],
|
|
156
|
+
"husnummer" => address["CVRAdresse_husnummerFra"],
|
|
157
|
+
"postnummer" => address["CVRAdresse_postnummer"],
|
|
158
|
+
"postdistrikt" => address["CVRAdresse_postdistrikt"]
|
|
159
|
+
}
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
def format_time(time)
|
|
163
|
+
time.getutc.strftime("%Y-%m-%dT%H:%M:%S.000000Z")
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
def first_node(payload)
|
|
167
|
+
nodes(payload).first
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
def nodes(payload)
|
|
171
|
+
data = payload["data"]
|
|
172
|
+
return [] unless data.is_a?(Hash)
|
|
173
|
+
|
|
174
|
+
connection = data["CVREnhed"] || data["CVR_Virksomhed"] || data["Virksomhed"] || data.values.first
|
|
175
|
+
return [] unless connection.is_a?(Hash)
|
|
176
|
+
|
|
177
|
+
Array(connection["nodes"] || connection["items"] || connection["edges"]&.filter_map do |e|
|
|
178
|
+
e.is_a?(Hash) ? e["node"] : nil
|
|
179
|
+
end)
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
def post_graphql(query, **variables)
|
|
183
|
+
uri = URI(base_url)
|
|
184
|
+
query_params = URI.decode_www_form(uri.query.to_s).to_h
|
|
185
|
+
query_params["apiKey"] = api_key
|
|
186
|
+
uri.query = URI.encode_www_form(query_params)
|
|
187
|
+
|
|
188
|
+
body = JSON.generate(query: query, variables: variables)
|
|
189
|
+
request = Net::HTTP::Post.new(uri)
|
|
190
|
+
request["Accept"] = "application/json"
|
|
191
|
+
request["Content-Type"] = "application/json"
|
|
192
|
+
request["User-Agent"] = "Cvrvaelger/#{VERSION}"
|
|
193
|
+
request.body = body
|
|
194
|
+
|
|
195
|
+
response = perform(uri, request)
|
|
196
|
+
raise ProviderError, "Datafordeler CVR error (#{response.code})" unless response.is_a?(Net::HTTPSuccess)
|
|
197
|
+
|
|
198
|
+
begin
|
|
199
|
+
payload = JSON.parse(response.body)
|
|
200
|
+
rescue JSON::ParserError
|
|
201
|
+
raise ProviderError, "Datafordeler CVR returned invalid JSON"
|
|
202
|
+
end
|
|
203
|
+
|
|
204
|
+
if payload["errors"].is_a?(Array) && payload["errors"].any?
|
|
205
|
+
message = payload["errors"].map { |err| err.is_a?(Hash) ? err["message"] : err }.compact.first
|
|
206
|
+
raise ProviderError, "Datafordeler CVR GraphQL error (#{message || 'unknown'})"
|
|
207
|
+
end
|
|
208
|
+
|
|
209
|
+
payload
|
|
210
|
+
end
|
|
211
|
+
|
|
212
|
+
def perform(uri, request)
|
|
213
|
+
return http.call(uri, request) if http
|
|
214
|
+
|
|
215
|
+
Net::HTTP.start(uri.host, uri.port, use_ssl: uri.scheme == "https", open_timeout: 10,
|
|
216
|
+
read_timeout: 30) do |client|
|
|
217
|
+
client.request(request)
|
|
218
|
+
end
|
|
219
|
+
rescue Net::OpenTimeout, Net::ReadTimeout, Errno::ECONNREFUSED, SocketError => e
|
|
220
|
+
raise ProviderError, "Datafordeler CVR unreachable (#{e.class})"
|
|
221
|
+
end
|
|
222
|
+
|
|
223
|
+
def blank?(value)
|
|
224
|
+
value.nil? || (value.respond_to?(:empty?) && value.empty?) ||
|
|
225
|
+
(value.is_a?(String) && value.strip.empty?)
|
|
226
|
+
end
|
|
227
|
+
|
|
228
|
+
def present?(value)
|
|
229
|
+
!blank?(value)
|
|
230
|
+
end
|
|
231
|
+
|
|
232
|
+
def present_string(value)
|
|
233
|
+
return nil if blank?(value)
|
|
234
|
+
|
|
235
|
+
value.to_s
|
|
236
|
+
end
|
|
237
|
+
end
|
|
238
|
+
end
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Cvrvaelger
|
|
4
|
+
PROVIDER = "datafordeler_cvr"
|
|
5
|
+
|
|
6
|
+
# Verified company hit from CVR (picker-shaped; no host-app persistence).
|
|
7
|
+
Company = Data.define(
|
|
8
|
+
:cvr,
|
|
9
|
+
:name,
|
|
10
|
+
:address_line,
|
|
11
|
+
:postal_code,
|
|
12
|
+
:city,
|
|
13
|
+
:provider,
|
|
14
|
+
:status
|
|
15
|
+
) do
|
|
16
|
+
def self.from_node(node)
|
|
17
|
+
cvr = normalize_cvr(node["cvrNummer"] || node["CVRNummer"] || node["cvr"])
|
|
18
|
+
name = extract_name(node)
|
|
19
|
+
address = node["adresse"] || node["beliggenhedsadresse"] || {}
|
|
20
|
+
address = {} unless address.is_a?(Hash)
|
|
21
|
+
|
|
22
|
+
new(
|
|
23
|
+
cvr: cvr,
|
|
24
|
+
name: name,
|
|
25
|
+
address_line: build_address_line(address, node),
|
|
26
|
+
postal_code: present_string(scalar(address["postnummer"] || address["postnr"] || node["postnummer"])),
|
|
27
|
+
city: present_string(scalar(address["postdistrikt"] || address["postnummerNavn"] || node["bynavn"])),
|
|
28
|
+
provider: PROVIDER,
|
|
29
|
+
status: present_string(scalar(node["status"] || node["virksomhedsstatus"]))
|
|
30
|
+
)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def self.normalize_cvr(value)
|
|
34
|
+
digits = value.to_s.gsub(/\D/, "")
|
|
35
|
+
digits.match?(/\A\d{8}\z/) ? digits : nil
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def self.extract_name(node)
|
|
39
|
+
raw = node["navn"] || node["navne"] || node["name"]
|
|
40
|
+
case raw
|
|
41
|
+
when String then present_string(raw)
|
|
42
|
+
when Hash then present_string(scalar(raw["navn"] || raw["name"]))
|
|
43
|
+
when Array
|
|
44
|
+
first = raw.find { |row| row.is_a?(Hash) } || raw.first
|
|
45
|
+
if first.is_a?(Hash)
|
|
46
|
+
present_string(scalar(first["navn"] || first["name"]))
|
|
47
|
+
else
|
|
48
|
+
present_string(first)
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def self.build_address_line(address, node)
|
|
54
|
+
if address["vejnavn"]
|
|
55
|
+
parts = [scalar(address["vejnavn"]), scalar(address["husnummer"] || address["husnr"])]
|
|
56
|
+
joined = parts.compact.map { |part| part.to_s.strip }.reject(&:empty?).join(" ")
|
|
57
|
+
return present_string(joined)
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
present_string(scalar(node["address"] || node["adressebetegnelse"]))
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def self.scalar(value)
|
|
64
|
+
return nil if value.nil? || value.is_a?(Hash) || value.is_a?(Array)
|
|
65
|
+
|
|
66
|
+
value
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def self.present_string(value)
|
|
70
|
+
return nil if value.nil?
|
|
71
|
+
|
|
72
|
+
string = value.to_s.strip
|
|
73
|
+
string.empty? ? nil : string
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
private_class_method :build_address_line, :scalar, :present_string
|
|
77
|
+
end
|
|
78
|
+
end
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Cvrvaelger
|
|
4
|
+
class Error < StandardError
|
|
5
|
+
end
|
|
6
|
+
|
|
7
|
+
# Raised when the Datafordeler CVR API fails, returns invalid JSON, or is unreachable.
|
|
8
|
+
class ProviderError < Error
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
# Raised when no API key is configured (no invented demo secret).
|
|
12
|
+
class ConfigurationError < Error
|
|
13
|
+
end
|
|
14
|
+
end
|
data/lib/cvrvaelger.rb
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "cvrvaelger/version"
|
|
4
|
+
require_relative "cvrvaelger/error"
|
|
5
|
+
require_relative "cvrvaelger/company"
|
|
6
|
+
require_relative "cvrvaelger/client"
|
|
7
|
+
|
|
8
|
+
# Ruby client for Det Centrale Virksomhedsregister (CVR) via Datafordeler GraphQL.
|
|
9
|
+
#
|
|
10
|
+
# Denmark only. Rails controllers, Stimulus pickers, and persistence belong in the
|
|
11
|
+
# host application (same split as adressevaelger).
|
|
12
|
+
module Cvrvaelger
|
|
13
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: cvrvaelger
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Rasmus Bergholdt
|
|
8
|
+
bindir: bin
|
|
9
|
+
cert_chain: []
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
|
+
dependencies: []
|
|
12
|
+
description: |
|
|
13
|
+
HTTP client for Det Centrale Virksomhedsregister (CVR) on Datafordeler GraphQL:
|
|
14
|
+
verify an 8-digit CVR number or exact registered name and return the company
|
|
15
|
+
name plus light address fields for picker UX. No Rails dependency.
|
|
16
|
+
email:
|
|
17
|
+
- rasmus.bergholdt@gmail.com
|
|
18
|
+
executables: []
|
|
19
|
+
extensions: []
|
|
20
|
+
extra_rdoc_files: []
|
|
21
|
+
files:
|
|
22
|
+
- ".env.example"
|
|
23
|
+
- ".rubocop.yml"
|
|
24
|
+
- CHANGELOG.md
|
|
25
|
+
- LICENSE.txt
|
|
26
|
+
- README.md
|
|
27
|
+
- cvrvaelger.gemspec
|
|
28
|
+
- lib/cvrvaelger.rb
|
|
29
|
+
- lib/cvrvaelger/client.rb
|
|
30
|
+
- lib/cvrvaelger/company.rb
|
|
31
|
+
- lib/cvrvaelger/error.rb
|
|
32
|
+
- lib/cvrvaelger/version.rb
|
|
33
|
+
homepage: https://github.com/bergholdt/cvrvaelger
|
|
34
|
+
licenses:
|
|
35
|
+
- MIT
|
|
36
|
+
metadata:
|
|
37
|
+
homepage_uri: https://github.com/bergholdt/cvrvaelger
|
|
38
|
+
source_code_uri: https://github.com/bergholdt/cvrvaelger/tree/v0.1.0
|
|
39
|
+
changelog_uri: https://github.com/bergholdt/cvrvaelger/blob/v0.1.0/CHANGELOG.md
|
|
40
|
+
bug_tracker_uri: https://github.com/bergholdt/cvrvaelger/issues
|
|
41
|
+
rubygems_mfa_required: 'true'
|
|
42
|
+
rdoc_options: []
|
|
43
|
+
require_paths:
|
|
44
|
+
- lib
|
|
45
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
46
|
+
requirements:
|
|
47
|
+
- - ">="
|
|
48
|
+
- !ruby/object:Gem::Version
|
|
49
|
+
version: 3.3.0
|
|
50
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
51
|
+
requirements:
|
|
52
|
+
- - ">="
|
|
53
|
+
- !ruby/object:Gem::Version
|
|
54
|
+
version: '0'
|
|
55
|
+
requirements: []
|
|
56
|
+
rubygems_version: 4.0.20
|
|
57
|
+
specification_version: 4
|
|
58
|
+
summary: Ruby client for Danish CVR company lookup via Datafordeler GraphQL.
|
|
59
|
+
test_files: []
|