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.
- checksums.yaml +7 -0
- data/CHANGELOG.md +26 -0
- data/README.md +595 -0
- data/lib/complyance/circuit_breaker.rb +99 -0
- data/lib/complyance/persistent_queue_manager.rb +474 -0
- data/lib/complyance/retry_strategy.rb +198 -0
- data/lib/complyance_sdk/config/retry_config.rb +127 -0
- data/lib/complyance_sdk/config/sdk_config.rb +212 -0
- data/lib/complyance_sdk/exceptions/circuit_breaker_open_error.rb +14 -0
- data/lib/complyance_sdk/exceptions/sdk_exception.rb +93 -0
- data/lib/complyance_sdk/generators/config_generator.rb +67 -0
- data/lib/complyance_sdk/generators/install_generator.rb +22 -0
- data/lib/complyance_sdk/generators/templates/complyance_initializer.rb +36 -0
- data/lib/complyance_sdk/http/authentication_middleware.rb +43 -0
- data/lib/complyance_sdk/http/client.rb +223 -0
- data/lib/complyance_sdk/http/logging_middleware.rb +153 -0
- data/lib/complyance_sdk/jobs/base_job.rb +63 -0
- data/lib/complyance_sdk/jobs/process_document_job.rb +92 -0
- data/lib/complyance_sdk/jobs/sidekiq_job.rb +165 -0
- data/lib/complyance_sdk/middleware/rack_middleware.rb +39 -0
- data/lib/complyance_sdk/models/country.rb +205 -0
- data/lib/complyance_sdk/models/country_policy_registry.rb +159 -0
- data/lib/complyance_sdk/models/document_type.rb +52 -0
- data/lib/complyance_sdk/models/environment.rb +144 -0
- data/lib/complyance_sdk/models/logical_doc_type.rb +228 -0
- data/lib/complyance_sdk/models/mode.rb +47 -0
- data/lib/complyance_sdk/models/operation.rb +47 -0
- data/lib/complyance_sdk/models/policy_result.rb +145 -0
- data/lib/complyance_sdk/models/purpose.rb +52 -0
- data/lib/complyance_sdk/models/source.rb +104 -0
- data/lib/complyance_sdk/models/source_ref.rb +130 -0
- data/lib/complyance_sdk/models/unify_request.rb +208 -0
- data/lib/complyance_sdk/models/unify_response.rb +198 -0
- data/lib/complyance_sdk/queue/persistent_queue_manager.rb +609 -0
- data/lib/complyance_sdk/railtie.rb +29 -0
- data/lib/complyance_sdk/retry/circuit_breaker.rb +159 -0
- data/lib/complyance_sdk/retry/retry_manager.rb +108 -0
- data/lib/complyance_sdk/retry/retry_strategy.rb +225 -0
- data/lib/complyance_sdk/version.rb +5 -0
- data/lib/complyance_sdk.rb +935 -0
- 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
|