io-complyance-unify-sdk 3.0.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.
Files changed (41) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +26 -0
  3. data/README.md +595 -0
  4. data/lib/complyance/circuit_breaker.rb +99 -0
  5. data/lib/complyance/persistent_queue_manager.rb +474 -0
  6. data/lib/complyance/retry_strategy.rb +198 -0
  7. data/lib/complyance_sdk/config/retry_config.rb +127 -0
  8. data/lib/complyance_sdk/config/sdk_config.rb +212 -0
  9. data/lib/complyance_sdk/exceptions/circuit_breaker_open_error.rb +14 -0
  10. data/lib/complyance_sdk/exceptions/sdk_exception.rb +93 -0
  11. data/lib/complyance_sdk/generators/config_generator.rb +67 -0
  12. data/lib/complyance_sdk/generators/install_generator.rb +22 -0
  13. data/lib/complyance_sdk/generators/templates/complyance_initializer.rb +36 -0
  14. data/lib/complyance_sdk/http/authentication_middleware.rb +43 -0
  15. data/lib/complyance_sdk/http/client.rb +223 -0
  16. data/lib/complyance_sdk/http/logging_middleware.rb +153 -0
  17. data/lib/complyance_sdk/jobs/base_job.rb +63 -0
  18. data/lib/complyance_sdk/jobs/process_document_job.rb +92 -0
  19. data/lib/complyance_sdk/jobs/sidekiq_job.rb +165 -0
  20. data/lib/complyance_sdk/middleware/rack_middleware.rb +39 -0
  21. data/lib/complyance_sdk/models/country.rb +205 -0
  22. data/lib/complyance_sdk/models/country_policy_registry.rb +159 -0
  23. data/lib/complyance_sdk/models/document_type.rb +52 -0
  24. data/lib/complyance_sdk/models/environment.rb +144 -0
  25. data/lib/complyance_sdk/models/logical_doc_type.rb +228 -0
  26. data/lib/complyance_sdk/models/mode.rb +47 -0
  27. data/lib/complyance_sdk/models/operation.rb +47 -0
  28. data/lib/complyance_sdk/models/policy_result.rb +145 -0
  29. data/lib/complyance_sdk/models/purpose.rb +52 -0
  30. data/lib/complyance_sdk/models/source.rb +104 -0
  31. data/lib/complyance_sdk/models/source_ref.rb +130 -0
  32. data/lib/complyance_sdk/models/unify_request.rb +208 -0
  33. data/lib/complyance_sdk/models/unify_response.rb +198 -0
  34. data/lib/complyance_sdk/queue/persistent_queue_manager.rb +609 -0
  35. data/lib/complyance_sdk/railtie.rb +29 -0
  36. data/lib/complyance_sdk/retry/circuit_breaker.rb +159 -0
  37. data/lib/complyance_sdk/retry/retry_manager.rb +108 -0
  38. data/lib/complyance_sdk/retry/retry_strategy.rb +225 -0
  39. data/lib/complyance_sdk/version.rb +5 -0
  40. data/lib/complyance_sdk.rb +935 -0
  41. metadata +322 -0
@@ -0,0 +1,67 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rails/generators"
4
+
5
+ module ComplyanceSDK
6
+ module Generators
7
+ # Generator for creating ComplyanceSDK configuration files
8
+ class ConfigGenerator < Rails::Generators::Base
9
+ source_root File.expand_path("templates", __dir__)
10
+
11
+ desc "Creates ComplyanceSDK configuration files"
12
+
13
+ class_option :format, type: :string, default: "initializer",
14
+ desc: "Configuration format (initializer, yaml, credentials)"
15
+
16
+ def create_config
17
+ case options[:format]
18
+ when "yaml"
19
+ create_yaml_config
20
+ when "credentials"
21
+ create_credentials_config
22
+ else
23
+ create_initializer_config
24
+ end
25
+ end
26
+
27
+ private
28
+
29
+ def create_initializer_config
30
+ template "complyance_initializer.rb", "config/initializers/complyance_sdk.rb"
31
+ end
32
+
33
+ def create_yaml_config
34
+ template "complyance_config.yml", "config/complyance_sdk.yml"
35
+ create_file "config/initializers/complyance_sdk.rb", <<~RUBY
36
+ # frozen_string_literal: true
37
+
38
+ # Load ComplyanceSDK configuration from YAML file
39
+ ComplyanceSDK.configure(
40
+ ComplyanceSDK::Config::SDKConfig.from_file(
41
+ Rails.root.join("config", "complyance_sdk.yml")
42
+ )
43
+ )
44
+ RUBY
45
+ end
46
+
47
+ def create_credentials_config
48
+ say "To use Rails credentials for ComplyanceSDK configuration:"
49
+ say ""
50
+ say "1. Run: rails credentials:edit"
51
+ say "2. Add the following structure:"
52
+ say ""
53
+ say "complyance:"
54
+ say " api_key: your_api_key_here"
55
+ say " environment: sandbox"
56
+ say ""
57
+
58
+ create_file "config/initializers/complyance_sdk.rb", <<~RUBY
59
+ # frozen_string_literal: true
60
+
61
+ # Configure ComplyanceSDK from Rails credentials
62
+ ComplyanceSDK.configure_from_rails
63
+ RUBY
64
+ end
65
+ end
66
+ end
67
+ end
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rails/generators"
4
+
5
+ module ComplyanceSDK
6
+ module Generators
7
+ # Generator for installing ComplyanceSDK in a Rails application
8
+ class InstallGenerator < Rails::Generators::Base
9
+ source_root File.expand_path("templates", __dir__)
10
+
11
+ desc "Creates a ComplyanceSDK initializer for your Rails application"
12
+
13
+ def copy_initializer
14
+ template "complyance_initializer.rb", "config/initializers/complyance_sdk.rb"
15
+ end
16
+
17
+ def show_readme
18
+ readme "README" if behavior == :invoke
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ # ComplyanceSDK configuration
4
+ ComplyanceSDK.configure do |config|
5
+ # API key for authentication
6
+ # You can set this using Rails credentials:
7
+ # config.api_key = Rails.application.credentials.dig(:complyance, :api_key)
8
+ # Or using environment variables:
9
+ config.api_key = ENV["COMPLYANCE_API_KEY"]
10
+
11
+ # Environment (sandbox, production)
12
+ # Default: :sandbox
13
+ config.environment = ENV.fetch("COMPLYANCE_ENVIRONMENT", "sandbox").to_sym
14
+
15
+ # Add sources
16
+ # config.add_source(
17
+ # ComplyanceSDK::Models::Source.new(
18
+ # id: "my-erp-system",
19
+ # type: :first_party,
20
+ # name: "My ERP System",
21
+ # version: "1.0"
22
+ # )
23
+ # )
24
+
25
+ # Configure retry behavior
26
+ # Default: RetryConfig.default
27
+ # config.retry_config = ComplyanceSDK::Config::RetryConfig.aggressive
28
+
29
+ # Enable or disable logging
30
+ # Default: true
31
+ config.logging_enabled = true
32
+
33
+ # Set log level (:debug, :info, :warn, :error)
34
+ # Default: :info
35
+ config.log_level = :info
36
+ end
@@ -0,0 +1,43 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ComplyanceSDK
4
+ module HTTP
5
+ # Faraday middleware for API key authentication
6
+ class AuthenticationMiddleware < Faraday::Middleware
7
+ # Initialize the middleware
8
+ #
9
+ # @param app [Object] The Faraday app
10
+ # @param api_key [String] The API key
11
+ def initialize(app, api_key)
12
+ super(app)
13
+ @api_key = api_key
14
+ end
15
+
16
+ # Process the request
17
+ #
18
+ # @param env [Hash] The request environment
19
+ def call(env)
20
+ # Add API key to headers
21
+ env[:request_headers]["Authorization"] = "Bearer #{@api_key}"
22
+ env[:request_headers]["X-API-Key"] = @api_key
23
+
24
+ # Add SDK identification headers
25
+ env[:request_headers]["User-Agent"] = "ComplyanceSDK-Ruby/#{ComplyanceSDK::VERSION}"
26
+ env[:request_headers]["X-SDK-Version"] = ComplyanceSDK::VERSION
27
+ env[:request_headers]["X-SDK-Language"] = "ruby"
28
+
29
+ # Add request ID for tracing
30
+ env[:request_headers]["X-Request-ID"] = generate_request_id
31
+
32
+ @app.call(env)
33
+ end
34
+
35
+ private
36
+
37
+ def generate_request_id
38
+ require "securerandom"
39
+ SecureRandom.uuid
40
+ end
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,223 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "faraday"
4
+ require "faraday/retry"
5
+ require "json"
6
+ require_relative "authentication_middleware"
7
+ require_relative "logging_middleware"
8
+
9
+ module ComplyanceSDK
10
+ module HTTP
11
+ # HTTP client for making requests to the Complyance API
12
+ class Client
13
+ # Default timeout in seconds
14
+ DEFAULT_TIMEOUT = 30
15
+
16
+ # Initialize a new HTTP client
17
+ #
18
+ # @param config [ComplyanceSDK::Config::SDKConfig] The SDK configuration
19
+ def initialize(config)
20
+ @config = config
21
+ @connection = build_connection
22
+ end
23
+
24
+ # Make a POST request to the API
25
+ #
26
+ # @param path [String] The API path
27
+ # @param payload [Hash] The request payload
28
+ # @param headers [Hash] Additional headers
29
+ # @return [Hash] The response data
30
+ def post(path, payload = {}, headers = {})
31
+ # Get the full URL
32
+ base_url = ComplyanceSDK::Models::Environment.base_url(@config.environment)
33
+ full_url = "#{base_url}#{path}"
34
+
35
+ # Log request details
36
+ puts "\n🌐 === API REQUEST ==="
37
+ puts "📍 URL: #{full_url}"
38
+ puts "📋 Headers: #{headers.merge('Content-Type' => 'application/json')}"
39
+ puts "📦 Payload:"
40
+ puts JSON.pretty_generate(payload)
41
+ puts "========================\n"
42
+
43
+ response = @connection.post(path) do |req|
44
+ req.headers.merge!(headers)
45
+ req.headers["Content-Type"] = "application/json"
46
+ req.body = payload.to_json
47
+ end
48
+
49
+ # Log response details
50
+ puts "\n📥 === API RESPONSE ==="
51
+ puts "📊 Status: #{response.status}"
52
+ puts "📋 Headers: #{response.headers}"
53
+ puts "📦 Body:"
54
+ puts JSON.pretty_generate(response.body) if response.body
55
+ puts "========================\n"
56
+
57
+ handle_response(response)
58
+ rescue Faraday::Error => e
59
+ puts "\n❌ === API ERROR ==="
60
+ puts "🚨 Error: #{e.class.name}"
61
+ puts "📝 Message: #{e.message}"
62
+ puts "===================\n"
63
+ handle_faraday_error(e)
64
+ end
65
+
66
+ # Make a GET request to the API
67
+ #
68
+ # @param path [String] The API path
69
+ # @param params [Hash] Query parameters
70
+ # @param headers [Hash] Additional headers
71
+ # @return [Hash] The response data
72
+ def get(path, params = {}, headers = {})
73
+ response = @connection.get(path) do |req|
74
+ req.headers.merge!(headers)
75
+ req.params.merge!(params)
76
+ end
77
+
78
+ handle_response(response)
79
+ rescue Faraday::Error => e
80
+ handle_faraday_error(e)
81
+ end
82
+
83
+ private
84
+
85
+ def parse_response_body(body)
86
+ return body if body.is_a?(Hash)
87
+ return nil if body.nil? || body.empty?
88
+
89
+ begin
90
+ JSON.parse(body, symbolize_names: true)
91
+ rescue JSON::ParserError
92
+ # If JSON parsing fails, return the original body
93
+ body
94
+ end
95
+ end
96
+
97
+ def build_connection
98
+ base_url = ComplyanceSDK::Models::Environment.base_url(@config.environment)
99
+
100
+ Faraday.new(url: base_url) do |conn|
101
+ # Request middleware
102
+ conn.request :json
103
+ conn.request :retry, retry_options
104
+
105
+ # Authentication middleware
106
+ conn.use AuthenticationMiddleware, @config.api_key
107
+
108
+ # Logging middleware
109
+ if @config.logging_enabled
110
+ conn.use LoggingMiddleware, @config.log_level
111
+ end
112
+
113
+ # Response middleware
114
+ conn.response :json, content_type: /\bjson$/
115
+
116
+ # HTTP adapter
117
+ conn.adapter Faraday.default_adapter
118
+
119
+ # Timeouts
120
+ conn.options.timeout = DEFAULT_TIMEOUT
121
+ conn.options.open_timeout = DEFAULT_TIMEOUT / 2
122
+ end
123
+ end
124
+
125
+ def retry_options
126
+ {
127
+ max: @config.retry_config.max_attempts - 1, # Faraday counts retries, not total attempts
128
+ interval: @config.retry_config.base_delay,
129
+ max_interval: @config.retry_config.max_delay,
130
+ backoff_factor: @config.retry_config.backoff_multiplier,
131
+ retry_statuses: @config.retry_config.retryable_http_codes,
132
+ methods: [:get, :post, :put, :patch, :delete],
133
+ exceptions: [
134
+ Faraday::ConnectionFailed,
135
+ Faraday::TimeoutError,
136
+ Faraday::SSLError
137
+ ]
138
+ }
139
+ end
140
+
141
+ def handle_response(response)
142
+ # Parse response body if it's a JSON string
143
+ parsed_body = parse_response_body(response.body)
144
+
145
+ case response.status
146
+ when 200..299
147
+ parsed_body || {}
148
+ when 400..499
149
+ error_message = parsed_body&.dig('message') || parsed_body&.dig('error_msg') || response.reason_phrase
150
+ raise ComplyanceSDK::Exceptions::APIError.new(
151
+ "Client error: #{error_message}",
152
+ status_code: response.status,
153
+ context: { response_body: parsed_body }
154
+ )
155
+ when 500..599
156
+ error_message = parsed_body&.dig('message') || parsed_body&.dig('error_msg') || response.reason_phrase
157
+ raise ComplyanceSDK::Exceptions::APIError.new(
158
+ "Server error: #{error_message}",
159
+ status_code: response.status,
160
+ context: { response_body: parsed_body }
161
+ )
162
+ else
163
+ error_message = parsed_body&.dig('message') || parsed_body&.dig('error_msg') || "Unexpected response"
164
+ raise ComplyanceSDK::Exceptions::APIError.new(
165
+ "#{error_message}: #{response.status}",
166
+ status_code: response.status,
167
+ context: { response_body: parsed_body }
168
+ )
169
+ end
170
+ end
171
+
172
+ def handle_faraday_error(error)
173
+ case error
174
+ when Faraday::ConnectionFailed
175
+ raise ComplyanceSDK::Exceptions::NetworkError.new(
176
+ "Connection failed: #{error.message}",
177
+ context: { original_error: error.class.name }
178
+ )
179
+ when Faraday::TimeoutError
180
+ raise ComplyanceSDK::Exceptions::NetworkError.new(
181
+ "Request timeout: #{error.message}",
182
+ context: { original_error: error.class.name }
183
+ )
184
+ when Faraday::SSLError
185
+ raise ComplyanceSDK::Exceptions::NetworkError.new(
186
+ "SSL error: #{error.message}",
187
+ context: { original_error: error.class.name }
188
+ )
189
+ else
190
+ # Handle Faraday::RetriableResponse (when retries are exhausted)
191
+ if error.class.name == "Faraday::RetriableResponse"
192
+ response = error.response
193
+ if response
194
+ # Check if this is a retryable error (5xx status codes) - matching Java SDK logic
195
+ # Access status code correctly from Faraday response object
196
+ status_code = response.respond_to?(:status) ? response.status : response[:status]
197
+ is_retryable = status_code && (status_code >= 500 || status_code == 429)
198
+
199
+ # Get response body correctly
200
+ response_body = response.respond_to?(:body) ? response.body : response[:body]
201
+ reason_phrase = response.respond_to?(:reason_phrase) ? response.reason_phrase : response[:reason_phrase]
202
+
203
+ raise ComplyanceSDK::Exceptions::APIError.new(
204
+ "Request failed after retries: #{response_body&.dig('message') || reason_phrase}",
205
+ status_code: status_code,
206
+ context: {
207
+ response_body: response_body,
208
+ retries_exhausted: true,
209
+ retryable: is_retryable
210
+ }
211
+ )
212
+ end
213
+ end
214
+
215
+ raise ComplyanceSDK::Exceptions::NetworkError.new(
216
+ "Network error: #{error.message}",
217
+ context: { original_error: error.class.name }
218
+ )
219
+ end
220
+ end
221
+ end
222
+ end
223
+ end
@@ -0,0 +1,153 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "logger"
4
+
5
+ module ComplyanceSDK
6
+ module HTTP
7
+ # Faraday middleware for request/response logging
8
+ class LoggingMiddleware < Faraday::Middleware
9
+ # Initialize the middleware
10
+ #
11
+ # @param app [Object] The Faraday app
12
+ # @param log_level [Symbol] The log level (:debug, :info, :warn, :error)
13
+ def initialize(app, log_level = :info)
14
+ super(app)
15
+ @log_level = log_level
16
+ @logger = Logger.new($stdout)
17
+ @logger.level = logger_level_constant(log_level)
18
+ end
19
+
20
+ # Process the request
21
+ #
22
+ # @param env [Hash] The request environment
23
+ def call(env)
24
+ start_time = Time.now
25
+
26
+ log_request(env)
27
+
28
+ @app.call(env).on_complete do |response_env|
29
+ end_time = Time.now
30
+ duration = ((end_time - start_time) * 1000).round(2)
31
+
32
+ log_response(response_env, duration)
33
+ end
34
+ end
35
+
36
+ private
37
+
38
+ def log_request(env)
39
+ return unless should_log?(:info)
40
+
41
+ method = env[:method].to_s.upcase
42
+ url = env[:url].to_s
43
+ headers = sanitize_headers(env[:request_headers])
44
+
45
+ @logger.info("ComplyanceSDK HTTP Request: #{method} #{url}")
46
+ @logger.debug("Request Headers: #{headers}") if should_log?(:debug)
47
+
48
+ if env[:body] && should_log?(:debug)
49
+ body = sanitize_body(env[:body])
50
+ @logger.debug("Request Body: #{body}")
51
+ end
52
+ end
53
+
54
+ def log_response(env, duration)
55
+ return unless should_log?(:info)
56
+
57
+ status = env[:status]
58
+ url = env[:url].to_s
59
+
60
+ @logger.info("ComplyanceSDK HTTP Response: #{status} #{url} (#{duration}ms)")
61
+
62
+ if should_log?(:debug)
63
+ headers = sanitize_headers(env[:response_headers])
64
+ @logger.debug("Response Headers: #{headers}")
65
+
66
+ if env[:body]
67
+ body = sanitize_body(env[:body])
68
+ @logger.debug("Response Body: #{body}")
69
+ end
70
+ end
71
+
72
+ # Log errors at warn level
73
+ if status >= 400 && should_log?(:warn)
74
+ @logger.warn("ComplyanceSDK HTTP Error: #{status} #{url}")
75
+ end
76
+ end
77
+
78
+ def sanitize_headers(headers)
79
+ return {} unless headers
80
+
81
+ sanitized = headers.dup
82
+
83
+ # Remove sensitive headers
84
+ %w[authorization x-api-key].each do |header|
85
+ if sanitized[header]
86
+ sanitized[header] = "[REDACTED]"
87
+ end
88
+ end
89
+
90
+ sanitized
91
+ end
92
+
93
+ def sanitize_body(body)
94
+ return body unless body.is_a?(String)
95
+
96
+ # Try to parse as JSON and sanitize
97
+ begin
98
+ parsed = JSON.parse(body)
99
+ sanitize_json_body(parsed).to_json
100
+ rescue JSON::ParserError
101
+ # If not JSON, return truncated body
102
+ body.length > 1000 ? "#{body[0..1000]}..." : body
103
+ end
104
+ end
105
+
106
+ def sanitize_json_body(data)
107
+ case data
108
+ when Hash
109
+ data.transform_values { |v| sanitize_json_body(v) }
110
+ when Array
111
+ data.map { |v| sanitize_json_body(v) }
112
+ when String
113
+ # Sanitize potential sensitive data
114
+ if data.match?(/\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b/)
115
+ "[EMAIL_REDACTED]"
116
+ elsif data.match?(/\b\d{4}[- ]?\d{4}[- ]?\d{4}[- ]?\d{4}\b/)
117
+ "[CARD_REDACTED]"
118
+ else
119
+ data
120
+ end
121
+ else
122
+ data
123
+ end
124
+ end
125
+
126
+ def should_log?(level)
127
+ level_value = logger_level_value(level)
128
+ current_level_value = logger_level_value(@log_level)
129
+ level_value >= current_level_value
130
+ end
131
+
132
+ def logger_level_value(level)
133
+ case level
134
+ when :debug then 0
135
+ when :info then 1
136
+ when :warn then 2
137
+ when :error then 3
138
+ else 1
139
+ end
140
+ end
141
+
142
+ def logger_level_constant(level)
143
+ case level
144
+ when :debug then Logger::DEBUG
145
+ when :info then Logger::INFO
146
+ when :warn then Logger::WARN
147
+ when :error then Logger::ERROR
148
+ else Logger::INFO
149
+ end
150
+ end
151
+ end
152
+ end
153
+ end
@@ -0,0 +1,63 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ComplyanceSDK
4
+ module Jobs
5
+ # Base job class for ComplyanceSDK background jobs
6
+ class BaseJob
7
+ # Include ActiveJob if available
8
+ if defined?(ActiveJob)
9
+ include ActiveJob::Base
10
+
11
+ # Set default queue
12
+ queue_as :complyance_sdk
13
+
14
+ # Configure retry behavior
15
+ retry_on ComplyanceSDK::Exceptions::NetworkError, wait: :exponentially_longer, attempts: 3
16
+ retry_on ComplyanceSDK::Exceptions::APIError, wait: :exponentially_longer, attempts: 2 do |job, error|
17
+ # Only retry certain API errors
18
+ error.status_code && [429, 500, 502, 503, 504].include?(error.status_code)
19
+ end
20
+
21
+ # Don't retry configuration or validation errors
22
+ discard_on ComplyanceSDK::Exceptions::ConfigurationError
23
+ discard_on ComplyanceSDK::Exceptions::ValidationError
24
+ end
25
+
26
+ # Perform the job (to be overridden by subclasses)
27
+ #
28
+ # @param args [Array] Job arguments
29
+ def perform(*args)
30
+ raise NotImplementedError, "Subclasses must implement #perform"
31
+ end
32
+
33
+ private
34
+
35
+ # Get the configured SDK client
36
+ #
37
+ # @return [ComplyanceSDK::HTTP::Client] The HTTP client
38
+ def sdk_client
39
+ unless ComplyanceSDK.configured?
40
+ raise ComplyanceSDK::Exceptions::ConfigurationError.new(
41
+ "ComplyanceSDK is not configured for background jobs"
42
+ )
43
+ end
44
+
45
+ @sdk_client ||= ComplyanceSDK::HTTP::Client.new(ComplyanceSDK.configuration)
46
+ end
47
+
48
+ # Log job execution
49
+ #
50
+ # @param message [String] The log message
51
+ # @param level [Symbol] The log level
52
+ # @param context [Hash] Additional context
53
+ def log_job(message, level: :info, context: {})
54
+ return unless defined?(Rails) && Rails.logger
55
+
56
+ full_message = "ComplyanceSDK Job [#{self.class.name}]: #{message}"
57
+ full_message += " Context: #{context}" unless context.empty?
58
+
59
+ Rails.logger.send(level, full_message)
60
+ end
61
+ end
62
+ end
63
+ end
@@ -0,0 +1,92 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "base_job"
4
+
5
+ module ComplyanceSDK
6
+ module Jobs
7
+ # Background job for processing documents asynchronously
8
+ class ProcessDocumentJob < BaseJob
9
+ # Process a document in the background
10
+ #
11
+ # @param request_data [Hash] The UnifyRequest data
12
+ # @param callback_url [String, nil] Optional callback URL for results
13
+ # @param callback_headers [Hash] Optional headers for callback
14
+ def perform(request_data, callback_url = nil, callback_headers = {})
15
+ log_job("Starting document processing", context: {
16
+ document_type: request_data["document_type"],
17
+ country: request_data["country"]
18
+ })
19
+
20
+ begin
21
+ # Process the document
22
+ response = sdk_client.post("/api/v1/unify", request_data)
23
+
24
+ log_job("Document processed successfully", context: {
25
+ response_status: response["status"],
26
+ submission_id: response.dig("data", "submission_id")
27
+ })
28
+
29
+ # Send callback if provided
30
+ if callback_url
31
+ send_callback(callback_url, callback_headers, {
32
+ status: "success",
33
+ data: response,
34
+ request_id: request_data["metadata"]&.dig("request_id")
35
+ })
36
+ end
37
+
38
+ response
39
+ rescue => error
40
+ log_job("Document processing failed", level: :error, context: {
41
+ error_class: error.class.name,
42
+ error_message: error.message
43
+ })
44
+
45
+ # Send error callback if provided
46
+ if callback_url
47
+ send_callback(callback_url, callback_headers, {
48
+ status: "error",
49
+ error: {
50
+ code: error.respond_to?(:code) ? error.code : "unknown_error",
51
+ message: error.message
52
+ },
53
+ request_id: request_data["metadata"]&.dig("request_id")
54
+ })
55
+ end
56
+
57
+ raise error
58
+ end
59
+ end
60
+
61
+ private
62
+
63
+ def send_callback(url, headers, payload)
64
+ require "net/http"
65
+ require "uri"
66
+ require "json"
67
+
68
+ uri = URI(url)
69
+ http = Net::HTTP.new(uri.host, uri.port)
70
+ http.use_ssl = uri.scheme == "https"
71
+
72
+ request = Net::HTTP::Post.new(uri)
73
+ request["Content-Type"] = "application/json"
74
+ headers.each { |key, value| request[key] = value }
75
+ request.body = payload.to_json
76
+
77
+ response = http.request(request)
78
+
79
+ log_job("Callback sent", context: {
80
+ callback_url: url,
81
+ callback_status: response.code,
82
+ payload_status: payload[:status]
83
+ })
84
+ rescue => error
85
+ log_job("Callback failed", level: :warn, context: {
86
+ callback_url: url,
87
+ error: error.message
88
+ })
89
+ end
90
+ end
91
+ end
92
+ end