ee_e_business_register 0.3.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/.ee_business_register_credentials.yml.example +8 -0
- data/.rubocop.yml +8 -0
- data/CHANGELOG.md +93 -0
- data/CODE_OF_CONDUCT.md +132 -0
- data/LICENSE.txt +21 -0
- data/Makefile +392 -0
- data/README.md +1294 -0
- data/Rakefile +12 -0
- data/lib/ee_e_business_register/client.rb +224 -0
- data/lib/ee_e_business_register/configuration.rb +160 -0
- data/lib/ee_e_business_register/errors.rb +98 -0
- data/lib/ee_e_business_register/models/classifier.rb +28 -0
- data/lib/ee_e_business_register/models/company.rb +2363 -0
- data/lib/ee_e_business_register/models/trust.rb +47 -0
- data/lib/ee_e_business_register/services/classifier_service.rb +176 -0
- data/lib/ee_e_business_register/services/company_service.rb +400 -0
- data/lib/ee_e_business_register/services/trusts_service.rb +136 -0
- data/lib/ee_e_business_register/types.rb +24 -0
- data/lib/ee_e_business_register/validation.rb +367 -0
- data/lib/ee_e_business_register/version.rb +5 -0
- data/lib/ee_e_business_register.rb +481 -0
- data/sig/ee_e_business_register.rbs +4 -0
- metadata +212 -0
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module EeEBusinessRegister
|
|
4
|
+
module Services
|
|
5
|
+
class TrustsService
|
|
6
|
+
def initialize(client = Client.new)
|
|
7
|
+
@client = client
|
|
8
|
+
@credentials = EeEBusinessRegister.configuration.credentials
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def search_trusts(trust_name: nil, person_first_name: nil, person_last_name: nil,
|
|
12
|
+
person_birth_date: nil, person_id_code: nil, only_valid: false,
|
|
13
|
+
page: 1, per_page: 10)
|
|
14
|
+
|
|
15
|
+
params = build_trusts_params(
|
|
16
|
+
trust_name: trust_name,
|
|
17
|
+
person_first_name: person_first_name,
|
|
18
|
+
person_last_name: person_last_name,
|
|
19
|
+
person_birth_date: person_birth_date,
|
|
20
|
+
person_id_code: person_id_code,
|
|
21
|
+
only_valid: only_valid,
|
|
22
|
+
page: page,
|
|
23
|
+
per_page: per_page
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
response = @client.call(:usaldushaldused_v1, params)
|
|
27
|
+
parse_trusts_response(response)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def get_trust_by_name(trust_name, only_valid: true)
|
|
31
|
+
search_trusts(trust_name: trust_name, only_valid: only_valid)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def get_trusts_by_person(person_id_code: nil, person_first_name: nil,
|
|
35
|
+
person_last_name: nil, person_birth_date: nil,
|
|
36
|
+
only_valid: true)
|
|
37
|
+
search_trusts(
|
|
38
|
+
person_id_code: person_id_code,
|
|
39
|
+
person_first_name: person_first_name,
|
|
40
|
+
person_last_name: person_last_name,
|
|
41
|
+
person_birth_date: person_birth_date,
|
|
42
|
+
only_valid: only_valid
|
|
43
|
+
)
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
private
|
|
47
|
+
|
|
48
|
+
def build_trusts_params(trust_name: nil, person_first_name: nil, person_last_name: nil,
|
|
49
|
+
person_birth_date: nil, person_id_code: nil, only_valid: false,
|
|
50
|
+
page: 1, per_page: 10)
|
|
51
|
+
{
|
|
52
|
+
ariregister_kasutajanimi: @credentials[:username],
|
|
53
|
+
ariregister_parool: @credentials[:password],
|
|
54
|
+
ariregister_sessioon: @credentials[:session],
|
|
55
|
+
ariregister_valjundi_formaat: 'xml',
|
|
56
|
+
usaldushalduse_nimi: trust_name,
|
|
57
|
+
fyysilise_isiku_eesnimi: person_first_name,
|
|
58
|
+
fyysilise_isiku_perekonnanimi: person_last_name,
|
|
59
|
+
fyysilise_isiku_synniaeg: person_birth_date,
|
|
60
|
+
fyysilise_isiku_kood: person_id_code,
|
|
61
|
+
ainult_kehtivad: only_valid,
|
|
62
|
+
keel: EeEBusinessRegister.configuration.language,
|
|
63
|
+
evarv: per_page,
|
|
64
|
+
lehekylg: page
|
|
65
|
+
}.compact
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def parse_trusts_response(response)
|
|
69
|
+
return nil unless response && response[:keha]
|
|
70
|
+
|
|
71
|
+
body = response[:keha]
|
|
72
|
+
|
|
73
|
+
Models::Trusts.new(
|
|
74
|
+
items: parse_trust_items(body[:usaldushaldused]),
|
|
75
|
+
total_count: body[:leitud_arv],
|
|
76
|
+
page: response.dig(:paring, :lehekylg) || 1,
|
|
77
|
+
per_page: response.dig(:paring, :evarv) || 10
|
|
78
|
+
)
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def parse_trust_items(trusts_data)
|
|
82
|
+
return [] unless trusts_data && trusts_data[:item]
|
|
83
|
+
|
|
84
|
+
items = trusts_data[:item]
|
|
85
|
+
items = [items] unless items.is_a?(Array)
|
|
86
|
+
|
|
87
|
+
items.map { |trust| build_trust(trust) }
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def build_trust(trust_data)
|
|
91
|
+
Models::Trust.new(
|
|
92
|
+
trust_id: trust_data[:usaldushalduse_id],
|
|
93
|
+
name: trust_data[:nimi],
|
|
94
|
+
registration_date: trust_data[:registreerimise_kpv],
|
|
95
|
+
status: trust_data[:staatus],
|
|
96
|
+
country: trust_data[:riik],
|
|
97
|
+
country_text: trust_data[:riik_tekstina],
|
|
98
|
+
total_beneficial_owners: trust_data[:kasusaajate_arv_kokku],
|
|
99
|
+
hidden_beneficial_owners: trust_data[:peidetud_kasusaajate_arv],
|
|
100
|
+
absence_notice: trust_data[:lahknevusteade_puudumisest],
|
|
101
|
+
persons: build_trust_persons(trust_data[:isikud])
|
|
102
|
+
)
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def build_trust_persons(persons_data)
|
|
106
|
+
return [] unless persons_data && persons_data[:isik]
|
|
107
|
+
|
|
108
|
+
persons = persons_data[:isik]
|
|
109
|
+
persons = [persons] unless persons.is_a?(Array)
|
|
110
|
+
|
|
111
|
+
persons.map { |person| build_trust_person(person) }
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
def build_trust_person(person_data)
|
|
115
|
+
Models::TrustPerson.new(
|
|
116
|
+
role: person_data[:roll],
|
|
117
|
+
first_name: person_data[:eesnimi],
|
|
118
|
+
last_name: person_data[:nimi],
|
|
119
|
+
company_name: (!person_data[:eesnimi] && person_data[:nimi]) ? person_data[:nimi] : nil,
|
|
120
|
+
id_code: person_data[:isikukood],
|
|
121
|
+
foreign_id_code: person_data[:valis_kood],
|
|
122
|
+
foreign_id_country: person_data[:valis_kood_riik],
|
|
123
|
+
foreign_id_country_text: person_data[:valis_kood_riik_tekstina],
|
|
124
|
+
birth_date: person_data[:synniaeg],
|
|
125
|
+
address_country: person_data[:aadress_riik],
|
|
126
|
+
address_country_text: person_data[:aadress_riik_tesktina] || person_data[:aadress_riik_tekstina],
|
|
127
|
+
residence_country: person_data[:elukoht_riik],
|
|
128
|
+
residence_country_text: person_data[:elukoht_riik_tekstina],
|
|
129
|
+
start_date: person_data[:algus_kpv],
|
|
130
|
+
end_date: person_data[:lopp_kpv],
|
|
131
|
+
discrepancy_notice_submitted: person_data[:lahknevusteade_esitatud]
|
|
132
|
+
)
|
|
133
|
+
end
|
|
134
|
+
end
|
|
135
|
+
end
|
|
136
|
+
end
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "dry-types"
|
|
4
|
+
|
|
5
|
+
module EeEBusinessRegister
|
|
6
|
+
module Types
|
|
7
|
+
include Dry.Types()
|
|
8
|
+
|
|
9
|
+
RegistryCode = Strict::String.constrained(format: /\A\d{8}\z/)
|
|
10
|
+
|
|
11
|
+
CompanyStatus = Strict::String.enum(
|
|
12
|
+
"R", # Registered
|
|
13
|
+
"K", # Deleted
|
|
14
|
+
"L", # Liquidated
|
|
15
|
+
"N", # Active in liquidation
|
|
16
|
+
"S" # Active in reorganization
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
LegalForm = Strict::String
|
|
20
|
+
Language = Strict::String.enum("est", "eng")
|
|
21
|
+
|
|
22
|
+
Date = Strict::String.constrained(format: /\A\d{4}-\d{2}-\d{2}\z/) | Strict::String.constrained(format: /\A\d{2}\.\d{2}\.\d{4}\z/)
|
|
23
|
+
end
|
|
24
|
+
end
|
|
@@ -0,0 +1,367 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module EeEBusinessRegister
|
|
4
|
+
# Comprehensive input validation and sanitization module
|
|
5
|
+
#
|
|
6
|
+
# This module provides robust validation and sanitization for all user inputs
|
|
7
|
+
# that interact with the Estonian e-Business Register API. It serves multiple
|
|
8
|
+
# critical security and data integrity purposes:
|
|
9
|
+
#
|
|
10
|
+
# **Security Functions:**
|
|
11
|
+
# - Prevents XML/SOAP injection attacks by sanitizing malicious input
|
|
12
|
+
# - Blocks potential XSS and code injection attempts
|
|
13
|
+
# - Validates input formats to prevent API abuse and DoS attacks
|
|
14
|
+
#
|
|
15
|
+
# **Data Quality Functions:**
|
|
16
|
+
# - Ensures Estonian registry codes follow the correct 8-digit format
|
|
17
|
+
# - Validates Estonian personal identification codes with checksum verification
|
|
18
|
+
# - Standardizes date formats across different input types
|
|
19
|
+
# - Normalizes company and person names for consistent searching
|
|
20
|
+
#
|
|
21
|
+
# **API Protection:**
|
|
22
|
+
# - Implements rate limiting through pagination and result count validation
|
|
23
|
+
# - Prevents excessive time range queries that could overload the Estonian API
|
|
24
|
+
# - Enforces reasonable limits on bulk operations
|
|
25
|
+
#
|
|
26
|
+
# All validation methods follow a consistent pattern: they either return
|
|
27
|
+
# sanitized/validated data or raise ValidationError for invalid inputs.
|
|
28
|
+
# Methods that handle potentially dangerous content (like names for search)
|
|
29
|
+
# include XSS prevention and character filtering.
|
|
30
|
+
#
|
|
31
|
+
# @example Basic usage
|
|
32
|
+
# registry_code = Validation.validate_registry_code("16863232")
|
|
33
|
+
# company_name = Validation.validate_company_name("Sorbeet Payments OÜ")
|
|
34
|
+
# date_range = Validation.validate_time_interval("2023-01-01", "2023-01-31")
|
|
35
|
+
#
|
|
36
|
+
module Validation
|
|
37
|
+
# Exception raised when input validation fails
|
|
38
|
+
#
|
|
39
|
+
# This error is thrown when user input doesn't meet the required format,
|
|
40
|
+
# contains potentially dangerous content, or exceeds allowed limits.
|
|
41
|
+
# The error message provides specific guidance on what was wrong and
|
|
42
|
+
# how to fix it.
|
|
43
|
+
#
|
|
44
|
+
class ValidationError < StandardError; end
|
|
45
|
+
|
|
46
|
+
# Regular expression for validating Estonian company registry codes
|
|
47
|
+
# Estonian companies have unique 8-digit identification codes
|
|
48
|
+
REGISTRY_CODE_PATTERN = /\A\d{8}\z/.freeze
|
|
49
|
+
|
|
50
|
+
# Regular expression for validating Estonian personal identification codes
|
|
51
|
+
# Format: First digit (1-6) indicates century and gender, followed by 10 more digits
|
|
52
|
+
# 1,2 = 1800-1899, 3,4 = 1900-1999, 5,6 = 2000-2099
|
|
53
|
+
# Odd numbers = male, even numbers = female
|
|
54
|
+
PERSONAL_CODE_PATTERN = /\A[1-6]\d{10}\z/.freeze
|
|
55
|
+
|
|
56
|
+
# Supported date format patterns for input validation
|
|
57
|
+
# Covers standard ISO formats and Estonian DD.MM.YYYY format
|
|
58
|
+
DATE_PATTERNS = [
|
|
59
|
+
/\A\d{4}-\d{2}-\d{2}\z/, # YYYY-MM-DD (ISO date)
|
|
60
|
+
/\A\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}Z?\z/, # ISO 8601 datetime with optional Z
|
|
61
|
+
/\A\d{2}\.\d{2}\.\d{4}\z/ # DD.MM.YYYY (Estonian format)
|
|
62
|
+
].freeze
|
|
63
|
+
|
|
64
|
+
# Supported language codes for API responses
|
|
65
|
+
# 'est' = Estonian, 'eng' = English
|
|
66
|
+
VALID_LANGUAGES = %w[est eng].freeze
|
|
67
|
+
|
|
68
|
+
# Common Estonian company legal form abbreviations
|
|
69
|
+
# OÜ = Osaühing (LLC), AS = Aktsiaselts (JSC), MTÜ = Mittetulundusühing (NPO), etc.
|
|
70
|
+
VALID_LEGAL_FORMS = %w[OÜ AS MTÜ SA TÜH UÜ].freeze
|
|
71
|
+
|
|
72
|
+
class << self
|
|
73
|
+
# Validate and sanitize registry code
|
|
74
|
+
# @param code [String, Integer] Registry code to validate
|
|
75
|
+
# @return [String] Sanitized 8-digit registry code
|
|
76
|
+
# @raise [ValidationError] If code format is invalid
|
|
77
|
+
def validate_registry_code(code)
|
|
78
|
+
return nil if code.nil? || code.to_s.strip.empty?
|
|
79
|
+
|
|
80
|
+
# Remove all non-numeric characters
|
|
81
|
+
sanitized = code.to_s.strip.gsub(/[^\d]/, '')
|
|
82
|
+
|
|
83
|
+
unless sanitized.match?(REGISTRY_CODE_PATTERN)
|
|
84
|
+
raise ValidationError,
|
|
85
|
+
"Invalid registry code format '#{code}'. Must be exactly 8 digits."
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
sanitized
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
# Validate Estonian personal identification code
|
|
92
|
+
# @param code [String] Personal code to validate
|
|
93
|
+
# @return [String, nil] Sanitized personal code or nil if invalid
|
|
94
|
+
def validate_personal_code(code)
|
|
95
|
+
return nil if code.nil? || code.to_s.strip.empty?
|
|
96
|
+
|
|
97
|
+
sanitized = code.to_s.strip.gsub(/[^\d]/, '')
|
|
98
|
+
|
|
99
|
+
return nil unless sanitized.match?(PERSONAL_CODE_PATTERN)
|
|
100
|
+
|
|
101
|
+
# Validate checksum for Estonian personal codes
|
|
102
|
+
return nil unless valid_personal_code_checksum?(sanitized)
|
|
103
|
+
|
|
104
|
+
sanitized
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
# Validate and sanitize company name for search
|
|
108
|
+
# @param name [String] Company name to validate
|
|
109
|
+
# @return [String, nil] Sanitized company name
|
|
110
|
+
def validate_company_name(name)
|
|
111
|
+
return nil if name.nil?
|
|
112
|
+
|
|
113
|
+
sanitized = name.to_s.strip
|
|
114
|
+
return nil if sanitized.empty?
|
|
115
|
+
|
|
116
|
+
# Remove potentially dangerous characters but keep Unicode letters
|
|
117
|
+
# Allow letters, numbers, spaces, and common business punctuation
|
|
118
|
+
sanitized = sanitized.gsub(/[<>"';&|`$(){}\[\]\\]/, '')
|
|
119
|
+
|
|
120
|
+
# Limit length to prevent DoS
|
|
121
|
+
sanitized = sanitized[0, 255] if sanitized.length > 255
|
|
122
|
+
|
|
123
|
+
sanitized.empty? ? nil : sanitized
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
# Validate person name (first/last name)
|
|
127
|
+
# @param name [String] Person name to validate
|
|
128
|
+
# @return [String, nil] Sanitized person name
|
|
129
|
+
def validate_person_name(name)
|
|
130
|
+
return nil if name.nil?
|
|
131
|
+
|
|
132
|
+
sanitized = name.to_s.strip
|
|
133
|
+
return nil if sanitized.empty?
|
|
134
|
+
|
|
135
|
+
# Allow only letters, spaces, hyphens, and apostrophes
|
|
136
|
+
sanitized = sanitized.gsub(/[^a-zA-ZÀ-ÿĀ-žА-я\s\-']/, '')
|
|
137
|
+
|
|
138
|
+
# Limit length
|
|
139
|
+
sanitized = sanitized[0, 100] if sanitized.length > 100
|
|
140
|
+
|
|
141
|
+
sanitized.empty? ? nil : sanitized
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
# Validate date input
|
|
145
|
+
# @param date [String, Date, Time] Date to validate
|
|
146
|
+
# @return [String, nil] ISO formatted date string
|
|
147
|
+
def validate_date(date)
|
|
148
|
+
return nil if date.nil?
|
|
149
|
+
|
|
150
|
+
case date
|
|
151
|
+
when Date
|
|
152
|
+
date.iso8601
|
|
153
|
+
when Time
|
|
154
|
+
date.to_date.iso8601
|
|
155
|
+
when String
|
|
156
|
+
sanitized = date.to_s.strip
|
|
157
|
+
return nil if sanitized.empty?
|
|
158
|
+
|
|
159
|
+
# Check if it matches expected patterns
|
|
160
|
+
return sanitized if DATE_PATTERNS.any? { |pattern| sanitized.match?(pattern) }
|
|
161
|
+
|
|
162
|
+
# Try to parse as date
|
|
163
|
+
begin
|
|
164
|
+
parsed_date = Date.parse(sanitized)
|
|
165
|
+
parsed_date.iso8601
|
|
166
|
+
rescue ArgumentError
|
|
167
|
+
raise ValidationError, "Invalid date format '#{date}'. Use YYYY-MM-DD format."
|
|
168
|
+
end
|
|
169
|
+
else
|
|
170
|
+
raise ValidationError, "Date must be a String, Date, or Time object"
|
|
171
|
+
end
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
# Validate language code
|
|
175
|
+
# @param language [String] Language code to validate
|
|
176
|
+
# @return [String] Valid language code
|
|
177
|
+
# @raise [ValidationError] If language is not supported
|
|
178
|
+
def validate_language(language)
|
|
179
|
+
return 'eng' if language.nil? || language.to_s.strip.empty?
|
|
180
|
+
|
|
181
|
+
lang = language.to_s.strip.downcase
|
|
182
|
+
|
|
183
|
+
unless VALID_LANGUAGES.include?(lang)
|
|
184
|
+
raise ValidationError,
|
|
185
|
+
"Invalid language '#{language}'. Must be one of: #{VALID_LANGUAGES.join(', ')}"
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
lang
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
# Validate page number for pagination
|
|
192
|
+
# @param page [Integer, String] Page number
|
|
193
|
+
# @return [Integer] Valid page number (minimum 1)
|
|
194
|
+
def validate_page_number(page)
|
|
195
|
+
return 1 if page.nil?
|
|
196
|
+
|
|
197
|
+
page_num = page.to_i
|
|
198
|
+
|
|
199
|
+
if page_num < 1
|
|
200
|
+
raise ValidationError, "Page number must be 1 or greater, got #{page}"
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
# Limit maximum page to prevent resource exhaustion
|
|
204
|
+
if page_num > 10000
|
|
205
|
+
raise ValidationError, "Page number too large (max 10000), got #{page}"
|
|
206
|
+
end
|
|
207
|
+
|
|
208
|
+
page_num
|
|
209
|
+
end
|
|
210
|
+
|
|
211
|
+
# Validate results per page limit
|
|
212
|
+
# @param limit [Integer, String] Number of results per page
|
|
213
|
+
# @param max_allowed [Integer] Maximum allowed limit
|
|
214
|
+
# @return [Integer] Valid limit
|
|
215
|
+
def validate_results_limit(limit, max_allowed = 100)
|
|
216
|
+
return 10 if limit.nil? # Default limit
|
|
217
|
+
|
|
218
|
+
limit_num = limit.to_i
|
|
219
|
+
|
|
220
|
+
if limit_num < 1
|
|
221
|
+
raise ValidationError, "Results limit must be 1 or greater, got #{limit}"
|
|
222
|
+
end
|
|
223
|
+
|
|
224
|
+
if limit_num > max_allowed
|
|
225
|
+
raise ValidationError,
|
|
226
|
+
"Results limit too large (max #{max_allowed}), got #{limit}"
|
|
227
|
+
end
|
|
228
|
+
|
|
229
|
+
limit_num
|
|
230
|
+
end
|
|
231
|
+
|
|
232
|
+
# Validate legal form codes
|
|
233
|
+
# @param forms [Array<String>] Legal form codes
|
|
234
|
+
# @return [Array<String>] Valid legal form codes
|
|
235
|
+
def validate_legal_forms(forms)
|
|
236
|
+
return [] if forms.nil? || forms.empty?
|
|
237
|
+
|
|
238
|
+
forms = [forms] unless forms.is_a?(Array)
|
|
239
|
+
|
|
240
|
+
validated = forms.map do |form|
|
|
241
|
+
sanitized = form.to_s.strip.upcase
|
|
242
|
+
next nil if sanitized.empty?
|
|
243
|
+
|
|
244
|
+
# Allow both official codes and common abbreviations
|
|
245
|
+
sanitized
|
|
246
|
+
end.compact
|
|
247
|
+
|
|
248
|
+
validated
|
|
249
|
+
end
|
|
250
|
+
|
|
251
|
+
# Sanitize XML input to prevent injection attacks
|
|
252
|
+
# @param input [String] Raw input that will be used in XML/SOAP
|
|
253
|
+
# @return [String] Sanitized input safe for XML
|
|
254
|
+
def sanitize_xml_input(input)
|
|
255
|
+
return '' if input.nil?
|
|
256
|
+
|
|
257
|
+
input.to_s
|
|
258
|
+
.gsub(/[<>]/, '') # Remove XML brackets
|
|
259
|
+
.gsub(/[&]/, '&') # Escape ampersands
|
|
260
|
+
.gsub(/["']/, '') # Remove quotes
|
|
261
|
+
.strip
|
|
262
|
+
end
|
|
263
|
+
|
|
264
|
+
# Validate array of registry codes (for bulk operations)
|
|
265
|
+
# @param codes [Array] Array of registry codes
|
|
266
|
+
# @param max_count [Integer] Maximum number of codes allowed
|
|
267
|
+
# @return [Array<String>] Array of validated registry codes
|
|
268
|
+
def validate_registry_codes_array(codes, max_count = 100)
|
|
269
|
+
if codes.nil? || codes.empty?
|
|
270
|
+
raise ValidationError, "Registry codes array cannot be empty"
|
|
271
|
+
end
|
|
272
|
+
|
|
273
|
+
codes = [codes] unless codes.is_a?(Array)
|
|
274
|
+
|
|
275
|
+
if codes.length > max_count
|
|
276
|
+
raise ValidationError,
|
|
277
|
+
"Too many registry codes (max #{max_count}), got #{codes.length}"
|
|
278
|
+
end
|
|
279
|
+
|
|
280
|
+
codes.map { |code| validate_registry_code(code) }.compact
|
|
281
|
+
end
|
|
282
|
+
|
|
283
|
+
# Validate time interval to prevent excessive API load
|
|
284
|
+
# @param start_time [Time, String] Start of time interval
|
|
285
|
+
# @param end_time [Time, String] End of time interval
|
|
286
|
+
# @param max_days [Integer] Maximum allowed interval in days
|
|
287
|
+
# @return [Array<Time>] Array of [start_time, end_time]
|
|
288
|
+
def validate_time_interval(start_time, end_time, max_days = 7)
|
|
289
|
+
start_t = parse_time_input(start_time)
|
|
290
|
+
end_t = parse_time_input(end_time)
|
|
291
|
+
|
|
292
|
+
unless start_t && end_t
|
|
293
|
+
raise ValidationError,
|
|
294
|
+
"Invalid time format. Use ISO 8601 format (YYYY-MM-DDTHH:MM:SSZ)"
|
|
295
|
+
end
|
|
296
|
+
|
|
297
|
+
if start_t >= end_t
|
|
298
|
+
raise ValidationError, "Start time must be before end time"
|
|
299
|
+
end
|
|
300
|
+
|
|
301
|
+
interval_days = (end_t - start_t) / 86400.0 # Convert seconds to days
|
|
302
|
+
|
|
303
|
+
if interval_days > max_days
|
|
304
|
+
raise ValidationError,
|
|
305
|
+
"Time interval too large (max #{max_days} days), got #{interval_days.round(1)} days"
|
|
306
|
+
end
|
|
307
|
+
|
|
308
|
+
# Prevent queries too far in the past (data availability)
|
|
309
|
+
max_history_days = 3650 # ~10 years
|
|
310
|
+
days_ago = (Time.now - start_t) / 86400.0
|
|
311
|
+
|
|
312
|
+
if days_ago > max_history_days
|
|
313
|
+
raise ValidationError,
|
|
314
|
+
"Start time too far in the past (max #{max_history_days} days ago)"
|
|
315
|
+
end
|
|
316
|
+
|
|
317
|
+
[start_t, end_t]
|
|
318
|
+
end
|
|
319
|
+
|
|
320
|
+
private
|
|
321
|
+
|
|
322
|
+
# Parse time input from various formats
|
|
323
|
+
# @param time_input [Time, String, Integer] Time in various formats
|
|
324
|
+
# @return [Time, nil] Parsed time or nil if invalid
|
|
325
|
+
def parse_time_input(time_input)
|
|
326
|
+
case time_input
|
|
327
|
+
when Time
|
|
328
|
+
time_input
|
|
329
|
+
when String
|
|
330
|
+
Time.parse(time_input)
|
|
331
|
+
when Integer
|
|
332
|
+
Time.at(time_input) # Unix timestamp
|
|
333
|
+
else
|
|
334
|
+
nil
|
|
335
|
+
end
|
|
336
|
+
rescue ArgumentError
|
|
337
|
+
nil
|
|
338
|
+
end
|
|
339
|
+
|
|
340
|
+
# Validate Estonian personal code checksum
|
|
341
|
+
# @param code [String] 11-digit personal code
|
|
342
|
+
# @return [Boolean] True if checksum is valid
|
|
343
|
+
def valid_personal_code_checksum?(code)
|
|
344
|
+
return false unless code.length == 11
|
|
345
|
+
|
|
346
|
+
digits = code.chars.map(&:to_i)
|
|
347
|
+
|
|
348
|
+
# First checksum calculation
|
|
349
|
+
weights1 = [1, 2, 3, 4, 5, 6, 7, 8, 9, 1]
|
|
350
|
+
sum1 = digits[0, 10].each_with_index.sum { |digit, i| digit * weights1[i] }
|
|
351
|
+
remainder1 = sum1 % 11
|
|
352
|
+
|
|
353
|
+
if remainder1 < 10
|
|
354
|
+
return digits[10] == remainder1
|
|
355
|
+
end
|
|
356
|
+
|
|
357
|
+
# Second checksum calculation if first gives 10
|
|
358
|
+
weights2 = [3, 4, 5, 6, 7, 8, 9, 1, 2, 3]
|
|
359
|
+
sum2 = digits[0, 10].each_with_index.sum { |digit, i| digit * weights2[i] }
|
|
360
|
+
remainder2 = sum2 % 11
|
|
361
|
+
|
|
362
|
+
checksum = remainder2 < 10 ? remainder2 : 0
|
|
363
|
+
digits[10] == checksum
|
|
364
|
+
end
|
|
365
|
+
end
|
|
366
|
+
end
|
|
367
|
+
end
|