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,104 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ComplyanceSDK
4
+ module Models
5
+ # Source type enumeration
6
+ module SourceType
7
+ # First-party source
8
+ FIRST_PARTY = :first_party
9
+
10
+ # Third-party source
11
+ THIRD_PARTY = :third_party
12
+
13
+ # Marketplace source
14
+ MARKETPLACE = :marketplace
15
+
16
+ # Get all valid source types
17
+ #
18
+ # @return [Array<Symbol>] All valid source types
19
+ def self.all
20
+ [FIRST_PARTY, THIRD_PARTY, MARKETPLACE]
21
+ end
22
+ end
23
+
24
+ # Source model representing the origin of documents
25
+ class Source
26
+ # Source ID
27
+ attr_accessor :id
28
+
29
+ # Source type
30
+ attr_accessor :type
31
+
32
+ # Source name
33
+ attr_accessor :name
34
+
35
+ # Source version
36
+ attr_accessor :version
37
+
38
+ # Source metadata
39
+ attr_accessor :metadata
40
+
41
+ # Initialize a new source
42
+ #
43
+ # @param options [Hash] Source options
44
+ # @option options [String] :id Source ID
45
+ # @option options [Symbol, String] :type Source type
46
+ # @option options [String] :name Source name
47
+ # @option options [String] :version Source version
48
+ # @option options [Hash] :metadata Source metadata
49
+ def initialize(options = {})
50
+ @id = options[:id]
51
+ @type = parse_source_type(options[:type])
52
+ @name = options[:name]
53
+ @version = options[:version]
54
+ @metadata = options[:metadata] || {}
55
+ end
56
+
57
+ # Convert the source to a hash
58
+ #
59
+ # @return [Hash] The source as a hash
60
+ def to_h
61
+ {
62
+ name: @name,
63
+ version: @version,
64
+ type: @type.to_s.upcase,
65
+ identity: "#{@name}:#{@version}",
66
+ id: "#{@name}:#{@version}"
67
+ }
68
+ end
69
+
70
+ # Convert the source to JSON
71
+ #
72
+ # @return [String] The source as JSON
73
+ def to_json(*args)
74
+ to_h.to_json(*args)
75
+ end
76
+
77
+ # Check if the source is valid
78
+ #
79
+ # @return [Boolean] True if valid, false otherwise
80
+ def valid?
81
+ !@id.nil? && !@id.empty? &&
82
+ !@name.nil? && !@name.empty? &&
83
+ SourceType.all.include?(@type)
84
+ end
85
+
86
+ private
87
+
88
+ def parse_source_type(type)
89
+ return SourceType::FIRST_PARTY if type.nil?
90
+
91
+ case type.to_s.downcase
92
+ when "first_party", "firstparty", "first-party"
93
+ SourceType::FIRST_PARTY
94
+ when "third_party", "thirdparty", "third-party"
95
+ SourceType::THIRD_PARTY
96
+ when "marketplace"
97
+ SourceType::MARKETPLACE
98
+ else
99
+ SourceType::FIRST_PARTY
100
+ end
101
+ end
102
+ end
103
+ end
104
+ end
@@ -0,0 +1,130 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ComplyanceSDK
4
+ module Models
5
+ # Reference to a source using name and version (simplified identity)
6
+ # This replaces the need for source ID bookkeeping on the client side
7
+ class SourceRef
8
+ attr_reader :name, :version, :type
9
+
10
+ # Initialize a new source reference
11
+ #
12
+ # @param name [String] The source name
13
+ # @param version [String] The source version
14
+ # @param type [Symbol, String] The source type (defaults to :first_party)
15
+ # @raise [ArgumentError] If name or version is blank
16
+ def initialize(name, version, type = :first_party)
17
+ # Allow empty values for mapping purposes
18
+ @name = name.nil? ? '' : name.to_s.strip
19
+ @version = version.nil? ? '' : version.to_s.strip
20
+ @type = type
21
+ end
22
+
23
+ # Get the identity string (name:version)
24
+ #
25
+ # @return [String] The identity string
26
+ def identity
27
+ if @name.empty? && @version.empty?
28
+ "mapping:unknown"
29
+ else
30
+ "#{@name}:#{@version}"
31
+ end
32
+ end
33
+
34
+ # Convert to hash representation
35
+ #
36
+ # @return [Hash] Hash representation of the source reference
37
+ def to_h
38
+ {
39
+ name: @name,
40
+ version: @version,
41
+ type: @type.to_s.upcase,
42
+ identity: identity,
43
+ id: identity
44
+ }
45
+ end
46
+
47
+ # Convert to JSON-compatible hash (for API requests)
48
+ #
49
+ # @return [Hash] JSON-compatible hash
50
+ def to_json_hash
51
+ {
52
+ 'name' => @name,
53
+ 'version' => @version
54
+ }
55
+ end
56
+
57
+ # Create from hash
58
+ #
59
+ # @param hash [Hash] Hash with name and version keys
60
+ # @return [SourceRef] New source reference instance
61
+ def self.from_h(hash)
62
+ new(hash[:name] || hash['name'], hash[:version] || hash['version'])
63
+ end
64
+
65
+ # String representation
66
+ #
67
+ # @return [String] String representation
68
+ def to_s
69
+ identity
70
+ end
71
+
72
+ # Inspect representation
73
+ #
74
+ # @return [String] Inspect representation
75
+ def inspect
76
+ "#<#{self.class.name}:#{object_id} name=#{@name.inspect} version=#{@version.inspect}>"
77
+ end
78
+
79
+ # Equality comparison
80
+ #
81
+ # @param other [SourceRef] Other source reference to compare
82
+ # @return [Boolean] True if equal
83
+ def ==(other)
84
+ return false unless other.is_a?(SourceRef)
85
+
86
+ @name == other.name && @version == other.version
87
+ end
88
+
89
+ # Hash code for equality
90
+ #
91
+ # @return [Integer] Hash code
92
+ def hash
93
+ [@name, @version].hash
94
+ end
95
+
96
+ # Alias for equality
97
+ alias_method :eql?, :==
98
+
99
+ # Validate the source reference
100
+ #
101
+ # @return [Boolean] True if valid
102
+ def valid?
103
+ !@name.empty? && !@version.empty?
104
+ end
105
+
106
+ # Get validation errors
107
+ #
108
+ # @return [Array<String>] Array of validation error messages
109
+ def validation_errors
110
+ errors = []
111
+ errors << 'Name cannot be blank' if @name.empty?
112
+ errors << 'Version cannot be blank' if @version.empty?
113
+ errors
114
+ end
115
+
116
+ # Check if source reference is valid, raise error if not
117
+ #
118
+ # @raise [ComplyanceSDK::Exceptions::ValidationError] If invalid
119
+ def validate!
120
+ return true if valid?
121
+
122
+ error_message = validation_errors.join(', ')
123
+ raise ComplyanceSDK::Exceptions::ValidationError.new(
124
+ "Invalid source reference: #{error_message}",
125
+ context: { name: @name, version: @version }
126
+ )
127
+ end
128
+ end
129
+ end
130
+ end
@@ -0,0 +1,208 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+
5
+ module ComplyanceSDK
6
+ module Models
7
+ # UnifyRequest model representing the main API request structure
8
+ class UnifyRequest
9
+ # Source of the request
10
+ attr_accessor :source
11
+
12
+ # Document type
13
+ attr_accessor :document_type
14
+
15
+ # Country code
16
+ attr_accessor :country
17
+
18
+ # Operation type
19
+ attr_accessor :operation
20
+
21
+ # Mode
22
+ attr_accessor :mode
23
+
24
+ # Purpose
25
+ attr_accessor :purpose
26
+
27
+ # Request payload
28
+ attr_accessor :payload
29
+
30
+ # Destinations
31
+ attr_accessor :destinations
32
+
33
+ # Request metadata
34
+ attr_accessor :metadata
35
+
36
+ # Initialize a new UnifyRequest
37
+ #
38
+ # @param options [Hash] Request options
39
+ # @option options [Source] :source The source of the request
40
+ # @option options [Symbol, String] :document_type The document type
41
+ # @option options [String] :country The country code
42
+ # @option options [Symbol, String] :operation The operation type
43
+ # @option options [Symbol, String] :mode The mode
44
+ # @option options [Symbol, String] :purpose The purpose
45
+ # @option options [Hash] :payload The request payload
46
+ # @option options [Array] :destinations The destinations
47
+ # @option options [Hash] :metadata The request metadata
48
+ def initialize(options = {})
49
+ @source = options[:source]
50
+ @document_type = DocumentType.normalize(options[:document_type])
51
+ @country = options[:country]
52
+ @operation = Operation.normalize(options[:operation])
53
+ @mode = Mode.normalize(options[:mode])
54
+ @purpose = Purpose.normalize(options[:purpose])
55
+ @payload = options[:payload] || {}
56
+ @destinations = options[:destinations] || []
57
+ @metadata = build_metadata(options[:metadata] || {})
58
+ end
59
+
60
+ # Check if the request is valid
61
+ #
62
+ # @return [Boolean] True if valid, false otherwise
63
+ def valid?
64
+ errors.empty?
65
+ end
66
+
67
+ # Get validation errors
68
+ #
69
+ # @return [Array<String>] Array of validation error messages
70
+ def errors
71
+ errors = []
72
+
73
+ errors << "Source is required" if @source.nil?
74
+ errors << "Source must be valid" if @source && !@source.valid?
75
+ errors << "Document type is required" if @document_type.nil?
76
+ errors << "Document type must be valid" unless DocumentType.valid?(@document_type)
77
+ errors << "Country is required" if @country.nil? || @country.empty?
78
+ errors << "Operation is required" if @operation.nil?
79
+ errors << "Operation must be valid" unless Operation.valid?(@operation)
80
+ errors << "Mode is required" if @mode.nil?
81
+ errors << "Mode must be valid" unless Mode.valid?(@mode)
82
+ errors << "Purpose is required" if @purpose.nil?
83
+ errors << "Purpose must be valid" unless Purpose.valid?(@purpose)
84
+ errors << "Payload is required" if @payload.nil? || @payload.empty?
85
+
86
+ errors
87
+ end
88
+
89
+ # Convert the request to a hash
90
+ #
91
+ # @return [Hash] The request as a hash
92
+ def to_h
93
+ {
94
+ source: @source&.to_h,
95
+ country: @country.to_s.upcase,
96
+ operation: @operation.to_s.upcase,
97
+ mode: @mode.to_s.upcase,
98
+ payload: @payload,
99
+ apiKey: @metadata[:api_key],
100
+ requestId: @metadata[:request_id],
101
+ timestamp: @metadata[:timestamp],
102
+ env: @metadata[:environment],
103
+ destinations: format_destinations(@destinations),
104
+ correlationId: @metadata[:correlation_id],
105
+ documentType: @document_type.to_s.upcase,
106
+ purpose: @purpose.to_s.downcase
107
+ }
108
+ end
109
+
110
+ # Convert the request to JSON
111
+ #
112
+ # @return [String] The request as JSON
113
+ def to_json(*args)
114
+ to_h.to_json(*args)
115
+ end
116
+
117
+ # Create a UnifyRequest from a hash
118
+ #
119
+ # @param hash [Hash] The hash to convert
120
+ # @return [UnifyRequest] The UnifyRequest object
121
+ def self.from_h(hash)
122
+ return nil unless hash.is_a?(Hash)
123
+
124
+ options = hash.dup
125
+
126
+ # Convert source hash to Source object if needed
127
+ if options[:source].is_a?(Hash)
128
+ options[:source] = Source.new(options[:source])
129
+ end
130
+
131
+ new(options)
132
+ end
133
+
134
+ # Create a UnifyRequest from JSON
135
+ #
136
+ # @param json [String] The JSON string to convert
137
+ # @return [UnifyRequest] The UnifyRequest object
138
+ def self.from_json(json)
139
+ hash = JSON.parse(json, symbolize_names: true)
140
+ from_h(hash)
141
+ rescue JSON::ParserError => e
142
+ raise ComplyanceSDK::Exceptions::ValidationError.new(
143
+ "Invalid JSON: #{e.message}",
144
+ context: { json: json }
145
+ )
146
+ end
147
+
148
+ private
149
+
150
+ def format_destinations(destinations)
151
+ return [] if destinations.nil? || destinations.empty?
152
+
153
+ destinations.map do |dest|
154
+ # Handle both symbol and string keys
155
+ dest = dest.is_a?(Hash) ? dest : dest.to_h
156
+
157
+ # Check if destination has nested details structure or flat structure
158
+ if dest.key?(:details) || dest.key?("details")
159
+ # Nested structure: {type: "...", details: {country: "...", authority: "...", document_type: "..."}}
160
+ details = dest[:details] || dest["details"] || {}
161
+ document_type = details[:document_type] || details["document_type"]
162
+ else
163
+ # Flat structure: {type: "...", country: "...", authority: "...", document_type: "..."}
164
+ details = dest
165
+ document_type = dest[:document_type] || dest["document_type"]
166
+ end
167
+
168
+ # Map document_type to documentType based on the actual document type
169
+ # Logic: if it contains "credit" -> credit_note, if it contains "debit" -> debit_note, else tax_invoice
170
+ if document_type&.downcase&.include?("credit")
171
+ document_type = "credit_note"
172
+ elsif document_type&.downcase&.include?("debit")
173
+ document_type = "debit_note"
174
+ elsif document_type == "tax_invoice"
175
+ document_type = "tax_invoice" # Keep as tax_invoice for regular invoices
176
+ end
177
+
178
+ {
179
+ type: (dest[:type] || dest["type"])&.to_s&.upcase,
180
+ details: {
181
+ country: (details[:country] || details["country"])&.to_s,
182
+ authority: (details[:authority] || details["authority"])&.to_s,
183
+ documentType: document_type&.to_s
184
+ }
185
+ }
186
+ end
187
+ end
188
+
189
+ def build_metadata(metadata)
190
+ default_metadata = {
191
+ api_key: nil, # Will be set by the HTTP client
192
+ request_id: generate_request_id,
193
+ timestamp: Time.now.utc.strftime('%Y-%m-%dT%H:%M:%SZ'),
194
+ environment: nil, # Will be set by the HTTP client
195
+ sdk_version: ComplyanceSDK::VERSION,
196
+ sdk_language: "ruby"
197
+ }
198
+
199
+ default_metadata.merge(metadata)
200
+ end
201
+
202
+ def generate_request_id
203
+ require "securerandom"
204
+ SecureRandom.uuid
205
+ end
206
+ end
207
+ end
208
+ end
@@ -0,0 +1,198 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+
5
+ module ComplyanceSDK
6
+ module Models
7
+ # UnifyResponse model representing the API response structure
8
+ class UnifyResponse
9
+ # Response status
10
+ attr_reader :status
11
+
12
+ # Response message
13
+ attr_reader :message
14
+
15
+ # Response data
16
+ attr_reader :data
17
+
18
+ # Error details
19
+ attr_reader :error
20
+
21
+ # Response metadata
22
+ attr_reader :metadata
23
+
24
+ # Initialize a new UnifyResponse
25
+ #
26
+ # @param options [Hash] Response options
27
+ # @option options [String] :status The response status
28
+ # @option options [String] :message The response message
29
+ # @option options [Hash] :data The response data
30
+ # @option options [Hash] :error The error details
31
+ # @option options [Hash] :metadata The response metadata
32
+ def initialize(options = {})
33
+ @status = options[:status]
34
+ @message = options[:message]
35
+ @data = options[:data]
36
+ @error = options[:error]
37
+ @metadata = options[:metadata] || {}
38
+ end
39
+
40
+ # Check if the response indicates success
41
+ #
42
+ # @return [Boolean] True if successful, false otherwise
43
+ def success?
44
+ (@status == "success" || @status == "completed") && !has_validation_errors?
45
+ end
46
+
47
+ # Check if the response indicates an error
48
+ #
49
+ # @return [Boolean] True if error, false otherwise
50
+ def error?
51
+ !success?
52
+ end
53
+
54
+ # Check if the response has validation errors
55
+ #
56
+ # @return [Boolean] True if validation errors exist, false otherwise
57
+ def has_validation_errors?
58
+ return false unless @data.is_a?(Hash)
59
+
60
+ validation_errors = @data.dig("validation", "errors") || @data.dig(:validation, :errors)
61
+ validation_errors.is_a?(Array) && !validation_errors.empty?
62
+ end
63
+
64
+ # Get validation errors from the response
65
+ #
66
+ # @return [Array] Array of validation error hashes
67
+ def validation_errors
68
+ return [] unless @data.is_a?(Hash)
69
+
70
+ validation_errors = @data.dig("validation", "errors") || @data.dig(:validation, :errors)
71
+ return [] unless validation_errors.is_a?(Array)
72
+
73
+ validation_errors
74
+ end
75
+
76
+ # Get the request ID from metadata
77
+ #
78
+ # @return [String, nil] The request ID
79
+ def request_id
80
+ @metadata[:request_id] || @metadata["request_id"]
81
+ end
82
+
83
+ # Get the processing time from metadata
84
+ #
85
+ # @return [Numeric, nil] The processing time in milliseconds
86
+ def processing_time
87
+ @metadata[:processing_time] || @metadata["processing_time"]
88
+ end
89
+
90
+ # Get the trace ID from metadata
91
+ #
92
+ # @return [String, nil] The trace ID
93
+ def trace_id
94
+ @metadata[:trace_id] || @metadata["trace_id"]
95
+ end
96
+
97
+ # Get the submission ID from data
98
+ # Checks multiple possible locations: data.submission_id, data.submission.submissionId, data.submission.submission_id
99
+ #
100
+ # @return [String, nil] The submission ID
101
+ def submission_id
102
+ return nil unless @data.is_a?(Hash)
103
+
104
+ # Check direct submission_id
105
+ submission_id = @data[:submission_id] || @data["submission_id"]
106
+ return submission_id if submission_id
107
+
108
+ # Check nested submission.submissionId or submission.submission_id
109
+ submission = @data[:submission] || @data["submission"]
110
+ return nil unless submission.is_a?(Hash)
111
+
112
+ submission[:submissionId] || submission["submissionId"] ||
113
+ submission[:submission_id] || submission["submission_id"]
114
+ end
115
+
116
+ # Convert the response to a hash
117
+ #
118
+ # @return [Hash] The response as a hash
119
+ def to_h
120
+ {
121
+ status: @status,
122
+ message: @message,
123
+ data: @data,
124
+ error: @error,
125
+ metadata: @metadata
126
+ }
127
+ end
128
+
129
+ # Convert the response to JSON
130
+ #
131
+ # @return [String] The response as JSON
132
+ def to_json(*args)
133
+ to_h.to_json(*args)
134
+ end
135
+
136
+ # Create a UnifyResponse from a hash
137
+ #
138
+ # @param hash [Hash] The hash to convert
139
+ # @return [UnifyResponse] The UnifyResponse object
140
+ def self.from_h(hash)
141
+ return nil unless hash.is_a?(Hash)
142
+
143
+ # Convert string keys to symbol keys for consistency
144
+ normalized_hash = {}
145
+ hash.each do |key, value|
146
+ symbol_key = key.is_a?(String) ? key.to_sym : key
147
+ normalized_hash[symbol_key] = value
148
+ end
149
+
150
+ new(normalized_hash)
151
+ end
152
+
153
+ # Create a UnifyResponse from JSON
154
+ #
155
+ # @param json [String] The JSON string to convert
156
+ # @return [UnifyResponse] The UnifyResponse object
157
+ def self.from_json(json)
158
+ hash = JSON.parse(json, symbolize_names: true)
159
+ from_h(hash)
160
+ rescue JSON::ParserError => e
161
+ raise ComplyanceSDK::Exceptions::ValidationError.new(
162
+ "Invalid JSON: #{e.message}",
163
+ context: { json: json }
164
+ )
165
+ end
166
+
167
+ # Create a success response
168
+ #
169
+ # @param data [Hash] The response data
170
+ # @param message [String] The success message
171
+ # @param metadata [Hash] The response metadata
172
+ # @return [UnifyResponse] The success response
173
+ def self.success(data = {}, message = "Success", metadata = {})
174
+ new(
175
+ status: "success",
176
+ message: message,
177
+ data: data,
178
+ metadata: metadata
179
+ )
180
+ end
181
+
182
+ # Create an error response
183
+ #
184
+ # @param error [Hash] The error details
185
+ # @param message [String] The error message
186
+ # @param metadata [Hash] The response metadata
187
+ # @return [UnifyResponse] The error response
188
+ def self.error(error = {}, message = "Error", metadata = {})
189
+ new(
190
+ status: "error",
191
+ message: message,
192
+ error: error,
193
+ metadata: metadata
194
+ )
195
+ end
196
+ end
197
+ end
198
+ end