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.
data/Rakefile ADDED
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bundler/gem_tasks"
4
+ require "minitest/test_task"
5
+
6
+ Minitest::TestTask.create
7
+
8
+ require "rubocop/rake_task"
9
+
10
+ RuboCop::RakeTask.new
11
+
12
+ task default: %i[test rubocop]
@@ -0,0 +1,224 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'savon'
4
+ require 'logger'
5
+
6
+ module EeEBusinessRegister
7
+ # SOAP client for the Estonian e-Business Register API
8
+ #
9
+ # This class handles all low-level communication with the Estonian e-Business Register
10
+ # SOAP API service. It manages authentication, request/response handling, error handling,
11
+ # and provides automatic retry logic for transient failures.
12
+ #
13
+ # The client is built on top of the Savon SOAP library and includes:
14
+ # - Automatic authentication using WSSE (Web Service Security Extensions)
15
+ # - Intelligent retry logic for rate limiting and timeouts
16
+ # - Comprehensive error handling with specific exception types
17
+ # - Optional logging for debugging and monitoring
18
+ #
19
+ # @example Basic usage
20
+ # client = Client.new(configuration)
21
+ # response = client.call(:lihtandmed_v2, ariregistri_kood: '16863232')
22
+ #
23
+ # @example With custom configuration
24
+ # config = Configuration.new
25
+ # config.username = 'api_user'
26
+ # config.password = 'api_pass'
27
+ # client = Client.new(config)
28
+ #
29
+ class Client
30
+ # @return [Configuration] The configuration object used by this client
31
+ attr_reader :config
32
+
33
+ # @return [Savon::Client] The underlying Savon SOAP client for testing
34
+ attr_reader :savon_client
35
+
36
+ # Initialize a new SOAP client with the provided configuration
37
+ #
38
+ # Sets up the underlying Savon SOAP client with proper authentication,
39
+ # timeouts, and error handling. The configuration is validated during
40
+ # initialization to ensure all required settings are present.
41
+ #
42
+ # @param config [Configuration] Configuration object with API credentials and settings
43
+ # @raise [ConfigurationError] If required configuration is missing or invalid
44
+ # @example
45
+ # config = Configuration.new
46
+ # config.username = 'myuser'
47
+ # config.password = 'mypass'
48
+ # client = Client.new(config)
49
+ #
50
+ def initialize(config = EeEBusinessRegister.configuration)
51
+ @config = config
52
+
53
+ # Validate configuration only if it has credentials
54
+ config.validate! if config.credentials_configured?
55
+
56
+ # Setup logging - use provided logger or create a null logger
57
+ @logger = config.logger || Logger.new(nil)
58
+
59
+ # Create and configure the underlying Savon SOAP client
60
+ # with Estonian e-Business Register specific settings
61
+ if config.credentials_configured?
62
+ @savon_client = Savon.client(
63
+ wsdl: config.wsdl_url, # WSDL location for service definition
64
+ endpoint: config.endpoint_url, # Actual service endpoint for requests
65
+ open_timeout: config.timeout, # Connection timeout
66
+ read_timeout: config.timeout, # Read timeout for responses
67
+ log: @logger.level == Logger::DEBUG, # Enable Savon logging only in debug mode
68
+ logger: @logger, # Logger for Savon internal logging
69
+ wsse_auth: [config.username, config.password], # WS-Security authentication
70
+ namespace_identifier: :ns1, # XML namespace prefix for requests
71
+ env_namespace: :soap, # SOAP envelope namespace
72
+ soap_version: 1, # Use SOAP 1.1 (required by Estonian API)
73
+ encoding: 'UTF-8', # Character encoding for XML
74
+ convert_request_keys_to: :none # Preserve original key names in requests
75
+ )
76
+ else
77
+ @savon_client = nil
78
+ end
79
+ end
80
+
81
+ # Check if the client is properly configured with valid credentials
82
+ #
83
+ # @return [Boolean] true if credentials are configured, false otherwise
84
+ def configured?
85
+ @config.credentials_configured?
86
+ end
87
+
88
+ # Get list of available SOAP operations
89
+ #
90
+ # @return [Array<Symbol>] List of available operations from the WSDL
91
+ def operations
92
+ return [] unless @savon_client
93
+ @savon_client.operations
94
+ end
95
+
96
+ # Execute a SOAP operation with automatic retry logic
97
+ #
98
+ # This method handles the actual SOAP request to the Estonian e-Business Register
99
+ # API. It includes intelligent retry logic for transient failures like rate limiting
100
+ # and timeouts, and converts various exception types into user-friendly APIErrors.
101
+ #
102
+ # The method automatically adds the configured language to all requests and
103
+ # handles the following error scenarios:
104
+ # - SOAP faults from the Estonian service
105
+ # - HTTP errors (401, 403, 429, 500, etc.)
106
+ # - Network timeouts with exponential backoff retry
107
+ # - Unexpected errors with proper logging
108
+ #
109
+ # @param operation [Symbol] The SOAP operation name to call (e.g., :lihtandmed_v2)
110
+ # @param message [Hash] Request parameters to send with the operation
111
+ # @return [Savon::Response] Raw SOAP response object
112
+ # @raise [APIError] For any API-related errors (authentication, service faults, etc.)
113
+ #
114
+ # @example Find company by registry code
115
+ # response = client.call(:lihtandmed_v2, ariregistri_kood: '16863232')
116
+ #
117
+ # @example Search companies by name
118
+ # response = client.call(:nimeparing_v1, nimi: 'Swedbank', max_arv: 10)
119
+ #
120
+ def call(operation, message = {})
121
+ # Ensure client is properly configured before making requests
122
+ unless configured?
123
+ raise AuthenticationError, 'Client is not properly configured. Please provide username and password.'
124
+ end
125
+
126
+ retries = 0
127
+ max_retries = 3
128
+
129
+ begin
130
+ # Log the operation being performed (if logging is enabled)
131
+ @logger.info "Calling #{operation} with params: #{message}" if @logger
132
+
133
+ # Ensure all requests include the configured language
134
+ # This tells the Estonian API which language to use for responses
135
+ message[:keel] = @config.language unless message.key?(:keel)
136
+
137
+ # Add authentication credentials to the message body
138
+ # The Estonian API expects credentials in the request body, not just WSSE headers
139
+ message[:ariregister_kasutajanimi] = @config.username
140
+ message[:ariregister_parool] = @config.password
141
+
142
+ # All operations use the "keha" wrapper for consistency
143
+ # The Estonian API expects all parameters wrapped in "keha"
144
+ wrapped_message = {
145
+ keha: message
146
+ }
147
+
148
+ # Execute the actual SOAP request via Savon
149
+ response = @savon_client.call(operation, message: wrapped_message)
150
+
151
+ # Log successful response (debug level to avoid spam)
152
+ @logger.debug "Response received: #{response.body}" if @logger
153
+
154
+ response
155
+
156
+ rescue Savon::SOAPFault => e
157
+ # Handle SOAP faults from the Estonian service
158
+ # These usually indicate business logic errors (company not found, etc.)
159
+ @logger.error "SOAP Fault: #{e.message}" if @logger
160
+ raise APIError, "SOAP Fault: #{e.message}"
161
+
162
+ rescue Savon::HTTPError => e
163
+ # Handle HTTP-level errors with special logic for rate limiting
164
+ if e.http.code == 429 && retries < max_retries
165
+ # Estonian API rate limiting - use exponential backoff
166
+ sleep_time = 2 ** retries
167
+ @logger.warn "Rate limited, retrying in #{sleep_time}s (attempt #{retries + 1}/#{max_retries})" if @logger
168
+ sleep(sleep_time)
169
+ retries += 1
170
+ retry
171
+ end
172
+
173
+ # Log and re-raise other HTTP errors
174
+ @logger.error "HTTP Error: #{e.http.code} - #{e.message}" if @logger
175
+ raise APIError, "HTTP Error: #{e.http.code} - #{e.message}"
176
+
177
+ rescue Timeout::Error, Net::ReadTimeout => e
178
+ # Handle network timeouts with retry logic
179
+ if retries < max_retries
180
+ retries += 1
181
+ @logger.warn "Timeout occurred, retrying (attempt #{retries}/#{max_retries})" if @logger
182
+ retry
183
+ end
184
+
185
+ # Max retries exceeded - give up
186
+ @logger.error "Request timeout after #{max_retries} attempts: #{e.message}" if @logger
187
+ raise APIError, "Request timeout: #{e.message}"
188
+
189
+ rescue => e
190
+ # Catch-all for unexpected errors - log details and re-raise as APIError
191
+ @logger.error "Unexpected error in SOAP call: #{e.class} - #{e.message}" if @logger
192
+ raise APIError, "Request failed: #{e.message}"
193
+ end
194
+ end
195
+
196
+ # Check if the Estonian e-Business Register API is accessible and responding
197
+ #
198
+ # Performs a simple API call to verify that the service is available and
199
+ # responding correctly. Uses a well-known Estonian company registry code
200
+ # (Estonian Business Registry itself - 10060701) for the test.
201
+ #
202
+ # This method swallows all exceptions and returns a simple boolean,
203
+ # making it safe to use for health checks and monitoring.
204
+ #
205
+ # @return [Boolean] true if API is accessible and responding, false otherwise
206
+ # @example
207
+ # if client.healthy?
208
+ # puts "API is working"
209
+ # else
210
+ # puts "API is down or unreachable"
211
+ # end
212
+ #
213
+ def healthy?
214
+ # Use Estonian Business Registry's own registry code for health check
215
+ # This is a stable, well-known entity that should always exist
216
+ call(:lihtandmed_v2, ariregistri_kood: '10060701')
217
+ true
218
+ rescue
219
+ # Any error means the API is not healthy
220
+ false
221
+ end
222
+ end
223
+
224
+ end
@@ -0,0 +1,160 @@
1
+ # frozen_string_literal: true
2
+
3
+ module EeEBusinessRegister
4
+ # Configuration class for the Estonian e-Business Register API client
5
+ #
6
+ # This class handles all configuration options required to connect to and interact
7
+ # with the Estonian e-Business Register SOAP API service. It supports both
8
+ # production and test environments with sensible defaults.
9
+ #
10
+ # @example Basic configuration
11
+ # config = Configuration.new
12
+ # config.username = 'your_username'
13
+ # config.password = 'your_password'
14
+ # config.validate!
15
+ #
16
+ # @example Test mode configuration
17
+ # config = Configuration.new
18
+ # config.username = 'test_user'
19
+ # config.password = 'test_pass'
20
+ # config.test_mode!
21
+ #
22
+ class Configuration
23
+ # API authentication credentials - required for all API calls
24
+ # @return [String] API username provided by Estonian Business Registry
25
+ attr_accessor :username
26
+
27
+ # @return [String] API password provided by Estonian Business Registry
28
+ attr_accessor :password
29
+
30
+ # SOAP service endpoints for the Estonian e-Business Register
31
+ # @return [String] WSDL URL for service definition (auto-set based on environment)
32
+ attr_accessor :wsdl_url
33
+
34
+ # @return [String] Endpoint URL for actual API calls (auto-set based on environment)
35
+ attr_accessor :endpoint_url
36
+
37
+ # API request configuration options
38
+ # @return [String] Response language: 'eng' for English, 'est' for Estonian (default: 'eng')
39
+ attr_accessor :language
40
+
41
+ # @return [Integer] HTTP request timeout in seconds (default: 30)
42
+ attr_accessor :timeout
43
+
44
+ # @return [Boolean] Whether using test environment endpoints (default: false)
45
+ attr_accessor :test_mode
46
+
47
+ # @return [Logger, nil] Optional logger instance for debugging API calls (default: nil)
48
+ attr_accessor :logger
49
+
50
+ # Initialize configuration with production defaults
51
+ #
52
+ # Sets up the configuration with sensible defaults for production use.
53
+ # You must set username and password before making API calls.
54
+ #
55
+ def initialize
56
+ # Production API endpoints - official Estonian e-Business Register
57
+ @wsdl_url = 'https://ariregxmlv6.rik.ee/?wsdl'
58
+ @endpoint_url = 'https://ariregxmlv6.rik.ee/'
59
+
60
+ # Default to English responses and 30-second timeout
61
+ @language = 'eng'
62
+ @timeout = 30
63
+
64
+ # Start in production mode without logging
65
+ @test_mode = false
66
+ @logger = nil
67
+ end
68
+
69
+ # Switch to test environment endpoints
70
+ #
71
+ # Changes the WSDL and endpoint URLs to use the Estonian e-Business Register
72
+ # test environment. Useful for development and testing without affecting
73
+ # production data or hitting rate limits.
74
+ #
75
+ # @return [Boolean] Always returns true
76
+ # @example
77
+ # config = Configuration.new
78
+ # config.test_mode!
79
+ # # Now uses test endpoints
80
+ #
81
+ def test_mode!
82
+ @test_mode = true
83
+ @wsdl_url = 'https://demo-ariregxmlv6.rik.ee/?wsdl'
84
+ @endpoint_url = 'https://demo-ariregxmlv6.rik.ee/'
85
+ end
86
+
87
+ # Validate that all required configuration is present and valid
88
+ #
89
+ # Checks that username and password are provided and that language
90
+ # is one of the supported options. Should be called before making
91
+ # any API requests to ensure proper configuration.
92
+ #
93
+ # @return [Boolean] Returns true if valid
94
+ # @raise [ConfigurationError] If any required setting is missing or invalid
95
+ # @example
96
+ # config.username = 'myuser'
97
+ # config.password = 'mypass'
98
+ # config.validate! # => true
99
+ #
100
+ def validate!
101
+ # Ensure API credentials are provided
102
+ raise ConfigurationError, 'Username is required' if username.nil? || username.empty?
103
+ raise ConfigurationError, 'Password is required' if password.nil? || password.empty?
104
+
105
+ # Validate language setting
106
+ unless %w[eng est].include?(language)
107
+ raise ConfigurationError, 'Language must be eng (English) or est (Estonian)'
108
+ end
109
+
110
+ true
111
+ end
112
+
113
+ # Switch to production environment endpoints
114
+ #
115
+ # Switches back to production URLs after being in test mode
116
+ # @return [Boolean] Always returns true
117
+ def production_mode!
118
+ @test_mode = false
119
+ @wsdl_url = 'https://ariregxmlv6.rik.ee/?wsdl'
120
+ @endpoint_url = 'https://ariregxmlv6.rik.ee/'
121
+ end
122
+
123
+ # Check if we're in test mode
124
+ #
125
+ # @return [Boolean] true if in test mode, false otherwise
126
+ def test_mode?
127
+ @test_mode == true
128
+ end
129
+
130
+ # Check if we're in production mode
131
+ #
132
+ # @return [Boolean] true if in production mode, false otherwise
133
+ def production_mode?
134
+ @test_mode != true
135
+ end
136
+
137
+ # Check if credentials are properly configured
138
+ #
139
+ # @return [Boolean] true if both username and password are present
140
+ def credentials_configured?
141
+ !username.nil? && !username.empty? && !password.nil? && !password.empty?
142
+ end
143
+
144
+ # Export configuration as hash
145
+ #
146
+ # @return [Hash] Configuration values with password masked for security
147
+ def to_h
148
+ {
149
+ wsdl_url: @wsdl_url,
150
+ endpoint_url: @endpoint_url,
151
+ language: @language,
152
+ timeout: @timeout,
153
+ username: @username,
154
+ password: @password.nil? ? nil : "[MASKED]",
155
+ environment: test_mode? ? "test" : "production"
156
+ }
157
+ end
158
+ end
159
+
160
+ end
@@ -0,0 +1,98 @@
1
+ # frozen_string_literal: true
2
+
3
+ module EeEBusinessRegister
4
+ # Base exception class for all Estonian e-Business Register gem errors
5
+ #
6
+ # This serves as the parent class for all custom exceptions raised by the gem.
7
+ # Catching this exception will capture any error originating from the gem's
8
+ # internal operations, making it useful for general error handling.
9
+ #
10
+ # @example Catch all gem-related errors
11
+ # begin
12
+ # company = EeEBusinessRegister.find_company('16863232')
13
+ # rescue EeEBusinessRegister::Error => e
14
+ # puts "Estonian register error: #{e.message}"
15
+ # end
16
+ #
17
+ class Error < StandardError; end
18
+
19
+ # Raised when API authentication fails
20
+ #
21
+ # This exception is thrown when the Estonian e-Business Register API
22
+ # rejects the provided username and password credentials. Common causes:
23
+ # - Incorrect username or password
24
+ # - Account suspended or expired
25
+ # - Network issues preventing proper authentication
26
+ #
27
+ # @example Handle authentication errors
28
+ # begin
29
+ # EeEBusinessRegister.find_company('16863232')
30
+ # rescue EeEBusinessRegister::AuthenticationError
31
+ # puts "Please check your API credentials"
32
+ # end
33
+ #
34
+ class AuthenticationError < Error; end
35
+
36
+ # Raised for all Estonian API communication and service errors
37
+ #
38
+ # This is the most common exception type, covering various API-related failures:
39
+ # - Network connectivity issues
40
+ # - Estonian service downtime or maintenance
41
+ # - Rate limiting (too many requests)
42
+ # - SOAP service faults (data not found, invalid requests)
43
+ # - HTTP errors (500, 503, etc.)
44
+ # - Request timeouts
45
+ #
46
+ # The error message typically includes specific details about what went wrong
47
+ # and suggestions for resolution (retry later, check parameters, etc.).
48
+ #
49
+ # @example Handle API errors gracefully
50
+ # begin
51
+ # company = EeEBusinessRegister.find_company('16863232')
52
+ # rescue EeEBusinessRegister::APIError => e
53
+ # if e.message.include?("timeout")
54
+ # puts "Service is slow, please try again"
55
+ # elsif e.message.include?("not found")
56
+ # puts "Company does not exist"
57
+ # else
58
+ # puts "API error: #{e.message}"
59
+ # end
60
+ # end
61
+ #
62
+ class APIError < Error; end
63
+
64
+ # Raised when gem configuration is invalid or incomplete
65
+ #
66
+ # This exception occurs when required configuration settings are missing
67
+ # or have invalid values. Most commonly raised during initial setup when
68
+ # username, password, or other required settings are not provided.
69
+ #
70
+ # @example Handle configuration errors
71
+ # begin
72
+ # EeEBusinessRegister.configure do |config|
73
+ # config.username = nil # This will cause an error
74
+ # end
75
+ # rescue EeEBusinessRegister::ConfigurationError => e
76
+ # puts "Configuration problem: #{e.message}"
77
+ # end
78
+ #
79
+ class ConfigurationError < Error; end
80
+
81
+ # Raised when user input fails validation rules
82
+ #
83
+ # This exception is thrown when user-provided data doesn't meet the
84
+ # required format or contains potentially dangerous content. Examples:
85
+ # - Registry codes that aren't exactly 8 digits
86
+ # - Invalid date formats
87
+ # - Empty or malformed search queries
88
+ # - Input that could cause security issues
89
+ #
90
+ # @example Handle validation errors
91
+ # begin
92
+ # company = EeEBusinessRegister.find_company('invalid-code')
93
+ # rescue EeEBusinessRegister::ValidationError => e
94
+ # puts "Input error: #{e.message}"
95
+ # end
96
+ #
97
+ class ValidationError < Error; end
98
+ end
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "dry-struct"
4
+
5
+ module EeEBusinessRegister
6
+ module Models
7
+ class ClassifierValue < Dry::Struct
8
+ attribute :code, Types::String
9
+ attribute :name, Types::String
10
+ attribute :valid_from, Types::String.optional
11
+ attribute :valid_to, Types::String.optional.default(nil)
12
+ end
13
+
14
+ class Classifier < Dry::Struct
15
+ attribute :code, Types::String
16
+ attribute :name, Types::String
17
+ attribute :values, Types::Array.of(ClassifierValue)
18
+
19
+ def find_value(code)
20
+ values.find { |v| v.code == code }
21
+ end
22
+
23
+ def active_values
24
+ values.select { |v| v.valid_to.nil? }
25
+ end
26
+ end
27
+ end
28
+ end