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,935 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "complyance_sdk/version"
4
+ require "complyance_sdk/config/sdk_config"
5
+ require "complyance_sdk/models/source"
6
+ require "complyance_sdk/models/source_ref"
7
+ require "complyance_sdk/models/environment"
8
+ require "complyance_sdk/models/document_type"
9
+ require "complyance_sdk/models/logical_doc_type"
10
+ require "complyance_sdk/models/country"
11
+ require "complyance_sdk/models/policy_result"
12
+ require "complyance_sdk/models/country_policy_registry"
13
+ require "complyance_sdk/models/operation"
14
+ require "complyance_sdk/models/mode"
15
+ require "complyance_sdk/models/purpose"
16
+ require "complyance_sdk/models/unify_request"
17
+ require "complyance_sdk/models/unify_response"
18
+ require "complyance_sdk/config/retry_config"
19
+ require "complyance_sdk/exceptions/sdk_exception"
20
+ require "complyance_sdk/exceptions/circuit_breaker_open_error"
21
+ # require "complyance_sdk/middleware/rack_middleware" # Removed - not needed
22
+ require "complyance_sdk/http/client"
23
+ # require "complyance_sdk/retry/retry_manager" # Removed - not needed
24
+ require "complyance_sdk/retry/circuit_breaker"
25
+ require "complyance_sdk/retry/retry_strategy"
26
+ require "complyance_sdk/queue/persistent_queue_manager"
27
+
28
+ # Main module for the Complyance SDK
29
+ module ComplyanceSDK
30
+ class << self
31
+ attr_accessor :configuration
32
+
33
+ # Configure the SDK with the provided configuration
34
+ #
35
+ # @param config [ComplyanceSDK::Config::SDKConfig] The configuration object
36
+ # @yield [config] Yields the configuration object for block-style configuration
37
+ # @return [ComplyanceSDK::Config::SDKConfig] The configuration object
38
+ def configure(config = nil)
39
+ self.configuration = config if config.is_a?(ComplyanceSDK::Config::SDKConfig)
40
+
41
+ if block_given?
42
+ self.configuration ||= ComplyanceSDK::Config::SDKConfig.new
43
+ yield(configuration)
44
+ end
45
+
46
+ configuration
47
+ end
48
+
49
+ # Configure the SDK from environment variables
50
+ #
51
+ # @return [ComplyanceSDK::Config::SDKConfig] The configuration object
52
+ def configure_from_env
53
+ self.configuration = ComplyanceSDK::Config::SDKConfig.from_env
54
+ end
55
+
56
+ # Configure the SDK from Rails credentials
57
+ #
58
+ # @param environment [Symbol] The Rails environment (:development, :production, etc.)
59
+ # @return [ComplyanceSDK::Config::SDKConfig] The configuration object
60
+ def configure_from_rails(environment = nil)
61
+ self.configuration = ComplyanceSDK::Config::SDKConfig.from_rails(environment)
62
+ end
63
+
64
+ # Check if the SDK is configured
65
+ #
66
+ # @return [Boolean] True if the SDK is configured, false otherwise
67
+ def configured?
68
+ !configuration.nil? && configuration.valid?
69
+ end
70
+
71
+ # Get the retry manager instance
72
+ #
73
+ # @param redis_config [Hash] Optional Redis configuration
74
+ # @return [ComplyanceSDK::Retry::RetryManager] The retry manager
75
+ def retry_manager(redis_config = {})
76
+ @retry_manager ||= ComplyanceSDK::Retry::RetryManager.new(configuration, redis_config)
77
+ end
78
+
79
+ # Execute an operation with retry logic
80
+ #
81
+ # @param operation_name [String] Name of the operation
82
+ # @param context [Hash] Context information
83
+ # @yield The block to execute
84
+ # @return The result of the block
85
+ def with_retry(operation_name, context = {})
86
+ unless configured?
87
+ raise ComplyanceSDK::Exceptions::ConfigurationError.new(
88
+ "SDK must be configured before using retry functionality"
89
+ )
90
+ end
91
+
92
+ retry_manager.execute(operation_name, context) { yield }
93
+ end
94
+
95
+ # Main API method for document processing (Java-style signature)
96
+ #
97
+ # @param source_name [String] The source name
98
+ # @param source_version [String] The source version
99
+ # @param logical_doc_type [Symbol] The logical document type
100
+ # @param country [Symbol] The country code
101
+ # @param operation [Symbol] The operation type
102
+ # @param mode [Symbol] The mode
103
+ # @param purpose [Symbol] The purpose
104
+ # @param payload [Hash] The business data payload
105
+ # @param destinations [Array, nil] Optional destinations (auto-generated if nil)
106
+ # @return [ComplyanceSDK::Models::UnifyResponse] The response object
107
+ def push_to_unify(source_name, source_version, logical_doc_type, country, operation, mode, purpose, payload, destinations = nil)
108
+ unless configured?
109
+ raise ComplyanceSDK::Exceptions::ConfigurationError.new(
110
+ "SDK must be configured before making API calls"
111
+ )
112
+ end
113
+
114
+ # Process queued submissions first before handling new requests
115
+ process_queued_submissions_first
116
+
117
+ # Validate required parameters
118
+ validate_required_parameter(source_name, :source_name, "Source name is required")
119
+ validate_required_parameter(source_version, :source_version, "Source version is required")
120
+ validate_required_parameter(logical_doc_type, :logical_doc_type, "Logical document type is required")
121
+ validate_required_parameter(country, :country, "Country is required")
122
+ validate_required_parameter(operation, :operation, "Operation is required")
123
+ validate_required_parameter(mode, :mode, "Mode is required")
124
+ validate_required_parameter(purpose, :purpose, "Purpose is required")
125
+ validate_required_parameter(payload, :payload, "Payload is required")
126
+
127
+ # Validate country restrictions for current environment
128
+ validate_country_for_environment(country, configuration.environment)
129
+
130
+ # Evaluate country policy to get base document type and meta.config flags
131
+ policy = ComplyanceSDK::Models::CountryPolicyRegistry.evaluate(country, logical_doc_type)
132
+
133
+ # Merge meta.config flags into payload
134
+ merged_payload = deep_merge_into_meta_config(payload, policy.get_meta_config_flags)
135
+
136
+ # Auto-set invoice_data.document_type based on LogicalDocType
137
+ set_invoice_data_document_type(merged_payload, logical_doc_type)
138
+
139
+ # Create source reference with type from configuration
140
+ source_type = find_source_type(source_name, source_version)
141
+ source_ref = ComplyanceSDK::Models::SourceRef.new(source_name, source_version, source_type)
142
+
143
+ # Auto-generate destinations if none provided and auto-generation is enabled
144
+ final_destinations = destinations || (configuration.auto_generate_tax_destination? ?
145
+ generate_default_destinations(country, policy.get_document_type) : [])
146
+
147
+ # Extract destinations from payload if they exist there
148
+ if final_destinations.empty? && merged_payload.is_a?(Hash) && merged_payload['destinations']
149
+ final_destinations = merged_payload['destinations']
150
+ # Remove destinations from payload to avoid duplication
151
+ merged_payload = merged_payload.dup
152
+ merged_payload.delete('destinations')
153
+ end
154
+
155
+ # Build and send request using the resolved base document type
156
+ push_to_unify_internal_with_document_type(
157
+ source_ref,
158
+ policy.base_document_type,
159
+ ComplyanceSDK::Models::LogicalDocType.meta_config_document_type(logical_doc_type),
160
+ country,
161
+ operation,
162
+ mode,
163
+ purpose,
164
+ merged_payload,
165
+ final_destinations
166
+ )
167
+ end
168
+
169
+ # Push to Unify API with logical document types using JSON string payload
170
+ #
171
+ # @param source_name [String] The source name
172
+ # @param source_version [String] The source version
173
+ # @param logical_doc_type [Symbol] The logical document type
174
+ # @param country [Symbol] The country code
175
+ # @param operation [Symbol] The operation type
176
+ # @param mode [Symbol] The mode
177
+ # @param purpose [Symbol] The purpose
178
+ # @param json_payload [String] The JSON string payload
179
+ # @param destinations [Array, nil] Optional destinations (auto-generated if nil)
180
+ # @return [ComplyanceSDK::Models::UnifyResponse] The response object
181
+ def push_to_unify_from_json(source_name, source_version, logical_doc_type, country, operation, mode, purpose, json_payload, destinations = nil)
182
+ if json_payload.nil? || json_payload.to_s.strip.empty?
183
+ raise ComplyanceSDK::Exceptions::ValidationError.new(
184
+ "Payload is required but was null or empty",
185
+ context: {
186
+ suggestion: 'Provide a non-empty JSON payload string. Example: \'{"invoiceNumber":"INV-123","amount":1000}\''
187
+ }
188
+ )
189
+ end
190
+
191
+ begin
192
+ require 'json'
193
+ payload_hash = JSON.parse(json_payload)
194
+
195
+ unless payload_hash.is_a?(Hash)
196
+ raise ComplyanceSDK::Exceptions::ValidationError.new(
197
+ "Failed to parse JSON payload: parsed result is not a hash",
198
+ context: {
199
+ suggestion: 'Ensure the payload is valid JSON and represents an object structure. Example: \'{"invoiceNumber":"INV-123"}\''
200
+ }
201
+ )
202
+ end
203
+
204
+ push_to_unify(source_name, source_version, logical_doc_type, country, operation, mode, purpose, payload_hash, destinations)
205
+ rescue JSON::ParserError => parse_error
206
+ payload_snippet = json_payload.length > 100 ? json_payload[0..99] + '...' : json_payload
207
+ raise ComplyanceSDK::Exceptions::ValidationError.new(
208
+ "Failed to parse JSON payload: #{parse_error.message}",
209
+ context: {
210
+ suggestion: 'Ensure the payload is valid JSON. Example: \'{"invoiceNumber":"INV-123","amount":1000}\'',
211
+ payload_snippet: payload_snippet,
212
+ parse_error: parse_error.message
213
+ }
214
+ )
215
+ rescue ComplyanceSDK::Exceptions::ValidationError
216
+ raise
217
+ rescue StandardError => conversion_error
218
+ raise ComplyanceSDK::Exceptions::ValidationError.new(
219
+ "Failed to parse JSON payload: #{conversion_error.message}",
220
+ context: {
221
+ suggestion: 'Ensure the payload is valid JSON. Example: \'{"invoiceNumber":"INV-123","amount":1000}\'',
222
+ conversion_error: conversion_error.message
223
+ }
224
+ )
225
+ end
226
+ end
227
+
228
+ # Push to Unify API with logical document types using object payload
229
+ #
230
+ # @param source_name [String] The source name
231
+ # @param source_version [String] The source version
232
+ # @param logical_doc_type [Symbol] The logical document type
233
+ # @param country [Symbol] The country code
234
+ # @param operation [Symbol] The operation type
235
+ # @param mode [Symbol] The mode
236
+ # @param purpose [Symbol] The purpose
237
+ # @param payload_object [Object] The object payload (any object that responds to to_h or has instance variables)
238
+ # @param destinations [Array, nil] Optional destinations (auto-generated if nil)
239
+ # @return [ComplyanceSDK::Models::UnifyResponse] The response object
240
+ def push_to_unify_from_object(source_name, source_version, logical_doc_type, country, operation, mode, purpose, payload_object, destinations = nil)
241
+ if payload_object.nil?
242
+ raise ComplyanceSDK::Exceptions::ValidationError.new(
243
+ "Payload object is required but was nil",
244
+ context: {
245
+ suggestion: "Provide a valid payload object. Example: {invoice_number: 'INV-123', amount: 1000}"
246
+ }
247
+ )
248
+ end
249
+
250
+ begin
251
+ # Convert object to hash
252
+ payload_hash = if payload_object.is_a?(Hash)
253
+ payload_object
254
+ elsif payload_object.respond_to?(:to_h)
255
+ payload_object.to_h
256
+ elsif payload_object.respond_to?(:instance_variables)
257
+ # Convert object instance variables to hash
258
+ hash = {}
259
+ payload_object.instance_variables.each do |var|
260
+ key = var.to_s.delete('@')
261
+ hash[key] = payload_object.instance_variable_get(var)
262
+ end
263
+ hash
264
+ else
265
+ # Try JSON serialization for other objects
266
+ require 'json'
267
+ json_str = payload_object.to_json
268
+ JSON.parse(json_str)
269
+ end
270
+
271
+ unless payload_hash.is_a?(Hash)
272
+ raise ComplyanceSDK::Exceptions::ValidationError.new(
273
+ "Failed to convert payload object to hash: conversion returned invalid result",
274
+ context: {
275
+ suggestion: "Ensure the object structure is compatible with the SDK payload format. " +
276
+ "The object should be convertible to a hash structure."
277
+ }
278
+ )
279
+ end
280
+
281
+ push_to_unify(source_name, source_version, logical_doc_type, country, operation, mode, purpose, payload_hash, destinations)
282
+ rescue ComplyanceSDK::Exceptions::ValidationError
283
+ raise
284
+ rescue StandardError => conversion_error
285
+ raise ComplyanceSDK::Exceptions::ValidationError.new(
286
+ "Failed to convert payload object to hash: #{conversion_error.message}",
287
+ context: {
288
+ suggestion: "Ensure the object structure is compatible with the SDK payload format. " +
289
+ "The object should be convertible to a hash. " +
290
+ "Example: {invoice_number: 'INV-123', amount: 1000} or a class with public attributes.",
291
+ object_type: payload_object.class.name,
292
+ conversion_error: conversion_error.message
293
+ }
294
+ )
295
+ end
296
+ end
297
+
298
+ # Main API method for document processing (UnifyRequest object)
299
+ #
300
+ # @param request [ComplyanceSDK::Models::UnifyRequest, Hash] The request object or hash
301
+ # @return [ComplyanceSDK::Models::UnifyResponse] The response object
302
+ def push_to_unify_request(request)
303
+ unless configured?
304
+ raise ComplyanceSDK::Exceptions::ConfigurationError.new(
305
+ "SDK must be configured before making API calls"
306
+ )
307
+ end
308
+
309
+ # Convert hash to UnifyRequest if needed
310
+ if request.is_a?(Hash)
311
+ request = ComplyanceSDK::Models::UnifyRequest.from_h(request)
312
+ end
313
+
314
+ # Validate the request
315
+ unless request.valid?
316
+ raise ComplyanceSDK::Exceptions::ValidationError.new(
317
+ "Invalid request: #{request.errors.join(', ')}",
318
+ context: { errors: request.errors }
319
+ )
320
+ end
321
+
322
+ # Make the API call with retry logic
323
+ response_data = with_retry("push_to_unify", { request_id: request.metadata[:request_id] }) do
324
+ http_client.post("", request.to_h)
325
+ end
326
+
327
+ # Convert response to UnifyResponse object
328
+ ComplyanceSDK::Models::UnifyResponse.from_h(response_data)
329
+ end
330
+
331
+ # Convenience method for submitting invoices
332
+ #
333
+ # @param options [Hash] Invoice submission options
334
+ # @option options [String] :country Country code
335
+ # @option options [Hash] :payload Invoice payload
336
+ # @option options [ComplyanceSDK::Models::Source] :source Source information
337
+ # @option options [Symbol] :document_type Document type (default: :tax_invoice)
338
+ # @option options [Symbol] :purpose Purpose (default: :invoicing)
339
+ # @return [ComplyanceSDK::Models::UnifyResponse] The response object
340
+ def submit_invoice(options = {})
341
+ request = ComplyanceSDK::Models::UnifyRequest.new(
342
+ source: options[:source] || default_source,
343
+ document_type: options[:document_type] || :tax_invoice,
344
+ country: options[:country],
345
+ operation: :single,
346
+ mode: :documents,
347
+ purpose: options[:purpose] || :invoicing,
348
+ payload: options[:payload]
349
+ )
350
+
351
+ push_to_unify(request)
352
+ end
353
+
354
+ # Convenience method for creating field mappings
355
+ #
356
+ # @param options [Hash] Mapping options
357
+ # @option options [String] :country Country code
358
+ # @option options [Hash] :payload Sample payload for mapping
359
+ # @option options [ComplyanceSDK::Models::Source] :source Source information
360
+ # @option options [Symbol] :document_type Document type (default: :tax_invoice)
361
+ # @return [ComplyanceSDK::Models::UnifyResponse] The response object
362
+ def create_mapping(options = {})
363
+ request = ComplyanceSDK::Models::UnifyRequest.new(
364
+ source: options[:source] || default_source,
365
+ document_type: options[:document_type] || :tax_invoice,
366
+ country: options[:country],
367
+ operation: :single,
368
+ mode: :documents,
369
+ purpose: :mapping,
370
+ payload: options[:payload]
371
+ )
372
+
373
+ push_to_unify(request)
374
+ end
375
+
376
+ # Get the status of a submission
377
+ #
378
+ # @param submission_id [String] The submission ID
379
+ # @return [ComplyanceSDK::Models::UnifyResponse] The response object
380
+ def get_status(submission_id)
381
+ unless configured?
382
+ raise ComplyanceSDK::Exceptions::ConfigurationError.new(
383
+ "SDK must be configured before making API calls"
384
+ )
385
+ end
386
+
387
+ response_data = with_retry("get_status", { submission_id: submission_id }) do
388
+ http_client.get("/api/v1/submissions/#{submission_id}")
389
+ end
390
+
391
+ ComplyanceSDK::Models::UnifyResponse.from_h(response_data)
392
+ end
393
+
394
+ # Process a document asynchronously using background jobs
395
+ #
396
+ # @param request [ComplyanceSDK::Models::UnifyRequest, Hash] The request object or hash
397
+ # @param options [Hash] Background job options
398
+ # @option options [Symbol] :job_type Job type (:active_job or :sidekiq)
399
+ # @option options [String] :callback_url Optional callback URL
400
+ # @option options [Hash] :callback_headers Optional callback headers
401
+ # @return [String, Object] Job ID or job object
402
+ def push_to_unify_async(request, options = {})
403
+ unless configured?
404
+ raise ComplyanceSDK::Exceptions::ConfigurationError.new(
405
+ "SDK must be configured before making API calls"
406
+ )
407
+ end
408
+
409
+ # Convert hash to UnifyRequest if needed
410
+ if request.is_a?(Hash)
411
+ request = ComplyanceSDK::Models::UnifyRequest.from_h(request)
412
+ end
413
+
414
+ # Validate the request
415
+ unless request.valid?
416
+ raise ComplyanceSDK::Exceptions::ValidationError.new(
417
+ "Invalid request: #{request.errors.join(', ')}",
418
+ context: { errors: request.errors }
419
+ )
420
+ end
421
+
422
+ retry_manager.execute_async(
423
+ request.to_h,
424
+ job_type: options[:job_type] || :active_job,
425
+ callback_url: options[:callback_url],
426
+ callback_headers: options[:callback_headers] || {}
427
+ )
428
+ end
429
+
430
+ # Push to Unify API with logical document types but full control over operation, mode, and purpose
431
+ # This is the primary method for all workflows with logical document types
432
+ #
433
+ # @param source_name [String] The source name
434
+ # @param source_version [String] The source version
435
+ # @param logical_type [Symbol] The logical document type
436
+ # @param country [Symbol] The country code
437
+ # @param operation [Symbol] The operation type
438
+ # @param mode [Symbol] The mode
439
+ # @param purpose [Symbol] The purpose
440
+ # @param payload [Hash] The business data payload
441
+ # @param destinations [Array, nil] Optional destinations (auto-generated if nil)
442
+ # @return [ComplyanceSDK::Models::UnifyResponse] The response object
443
+ def push_to_unify_logical(source_name, source_version, logical_type, country, operation, mode, purpose, payload, destinations = nil)
444
+ unless configured?
445
+ raise ComplyanceSDK::Exceptions::ConfigurationError.new(
446
+ "SDK must be configured before making API calls"
447
+ )
448
+ end
449
+
450
+ # Process queued submissions first before handling new requests
451
+ process_queued_submissions_first
452
+
453
+ # Validate required parameters
454
+ # Handle source_name and source_version based on purpose
455
+ final_source_name = if purpose == :mapping
456
+ # For MAPPING purpose, source_name and source_version are optional
457
+ source_name || ""
458
+ else
459
+ # For all other purposes, source_name is mandatory
460
+ if source_name.nil? || source_name.to_s.strip.empty?
461
+ raise ComplyanceSDK::Exceptions::ValidationError.new(
462
+ "Source name is required",
463
+ context: { field: :source_name }
464
+ )
465
+ end
466
+ source_name
467
+ end
468
+
469
+ final_source_version = if purpose == :mapping
470
+ # For MAPPING purpose, source_version is optional
471
+ source_version || ""
472
+ else
473
+ # For all other purposes, source_version is mandatory
474
+ if source_version.nil? || source_version.to_s.strip.empty?
475
+ raise ComplyanceSDK::Exceptions::ValidationError.new(
476
+ "Source version is required",
477
+ context: { field: :source_version }
478
+ )
479
+ end
480
+ source_version
481
+ end
482
+
483
+ # Validate other required parameters
484
+ validate_required_parameter(logical_type, :logical_type, "Logical document type is required")
485
+ validate_required_parameter(country, :country, "Country is required")
486
+ validate_required_parameter(operation, :operation, "Operation is required")
487
+ validate_required_parameter(mode, :mode, "Mode is required")
488
+ validate_required_parameter(purpose, :purpose, "Purpose is required")
489
+ validate_required_parameter(payload, :payload, "Payload is required")
490
+
491
+ # Validate country restrictions for current environment
492
+ validate_country_for_environment(country, configuration.environment)
493
+
494
+ # Evaluate country policy to get base document type and meta.config flags
495
+ policy = ComplyanceSDK::Models::CountryPolicyRegistry.evaluate(country, logical_type)
496
+
497
+ # Merge meta.config flags into payload
498
+ merged_payload = deep_merge_into_meta_config(payload, policy.get_meta_config_flags)
499
+
500
+ # Auto-set invoice_data.document_type based on LogicalDocType
501
+ set_invoice_data_document_type(merged_payload, logical_type)
502
+
503
+ # Create source reference with type from configuration
504
+ source_type = find_source_type(final_source_name, final_source_version)
505
+ source_ref = ComplyanceSDK::Models::SourceRef.new(final_source_name, final_source_version, source_type)
506
+
507
+ # Auto-generate destinations if none provided and auto-generation is enabled
508
+ final_destinations = destinations || (configuration.auto_generate_tax_destination? ?
509
+ generate_default_destinations(country, policy.get_document_type) : [])
510
+
511
+ # Build and send request using the resolved base document type
512
+ push_to_unify_internal_with_document_type(
513
+ source_ref,
514
+ policy.base_document_type,
515
+ ComplyanceSDK::Models::LogicalDocType.meta_config_document_type(logical_type),
516
+ country,
517
+ operation,
518
+ mode,
519
+ purpose,
520
+ merged_payload,
521
+ final_destinations
522
+ )
523
+ end
524
+
525
+ # Push to Unify API with logical document types using SourceRef
526
+ #
527
+ # @param source_ref [ComplyanceSDK::Models::SourceRef] The source reference
528
+ # @param logical_type [Symbol] The logical document type
529
+ # @param country [Symbol] The country code
530
+ # @param operation [Symbol] The operation type
531
+ # @param mode [Symbol] The mode
532
+ # @param purpose [Symbol] The purpose
533
+ # @param payload [Hash] The business data payload
534
+ # @param destinations [Array, nil] Optional destinations
535
+ # @return [ComplyanceSDK::Models::UnifyResponse] The response object
536
+ def push_to_unify_with_source_ref(source_ref, logical_type, country, operation, mode, purpose, payload, destinations = nil)
537
+ push_to_unify_logical(
538
+ source_ref.name,
539
+ source_ref.version,
540
+ logical_type,
541
+ country,
542
+ operation,
543
+ mode,
544
+ purpose,
545
+ payload,
546
+ destinations
547
+ )
548
+ end
549
+
550
+ # Convenience method to submit invoices with logical document types
551
+ #
552
+ # @param source_name [String] The source name
553
+ # @param source_version [String] The source version
554
+ # @param country [Symbol] The country code
555
+ # @param logical_type [Symbol] The logical document type
556
+ # @param payload [Hash] The business data payload
557
+ # @return [ComplyanceSDK::Models::UnifyResponse] The response object
558
+ def submit_invoice_logical(source_name, source_version, country, logical_type, payload)
559
+ push_to_unify_logical(
560
+ source_name,
561
+ source_version,
562
+ logical_type,
563
+ country,
564
+ :single,
565
+ :documents,
566
+ :invoicing,
567
+ payload
568
+ )
569
+ end
570
+
571
+ # Convenience method to create mappings with logical document types
572
+ #
573
+ # @param source_name [String] The source name
574
+ # @param source_version [String] The source version
575
+ # @param country [Symbol] The country code
576
+ # @param logical_type [Symbol] The logical document type
577
+ # @param payload [Hash] The business data payload
578
+ # @return [ComplyanceSDK::Models::UnifyResponse] The response object
579
+ def create_mapping_logical(source_name, source_version, country, logical_type, payload)
580
+ push_to_unify_logical(
581
+ source_name,
582
+ source_version,
583
+ logical_type,
584
+ country,
585
+ :single,
586
+ :documents,
587
+ :mapping,
588
+ payload
589
+ )
590
+ end
591
+
592
+ # Get queue status and statistics
593
+ #
594
+ # @return [Hash] Queue status information
595
+ def queue_status
596
+ if queue_manager
597
+ queue_manager.queue_status
598
+ else
599
+ { error: "Queue Manager is not initialized" }
600
+ end
601
+ end
602
+
603
+ # Get detailed queue status
604
+ #
605
+ # @return [Hash] Detailed queue status information
606
+ def detailed_queue_status
607
+ queue_status
608
+ end
609
+
610
+ # Retry failed submissions
611
+ def retry_failed_submissions
612
+ queue_manager&.retry_failed_submissions
613
+ end
614
+
615
+ # Clean up old success files
616
+ #
617
+ # @param days_to_keep [Integer] Number of days to keep success files
618
+ def cleanup_old_success_files(days_to_keep)
619
+ queue_manager&.cleanup_old_success_files(days_to_keep)
620
+ end
621
+
622
+ # Clear all files from the queue (emergency cleanup)
623
+ def clear_all_queues
624
+ if queue_manager
625
+ queue_manager.clear_all_queues
626
+ else
627
+ raise ComplyanceSDK::Exceptions::ConfigurationError.new("Queue Manager is not initialized")
628
+ end
629
+ end
630
+
631
+ # Clean up duplicate files across queue directories
632
+ def cleanup_duplicate_files
633
+ if queue_manager
634
+ queue_manager.cleanup_duplicate_files
635
+ else
636
+ raise ComplyanceSDK::Exceptions::ConfigurationError.new("Queue Manager is not initialized")
637
+ end
638
+ end
639
+
640
+ # Process pending submissions now
641
+ def process_pending_submissions
642
+ queue_manager&.process_pending_submissions_now
643
+ end
644
+
645
+ # Process queued submissions before handling new requests
646
+ def process_queued_submissions_first
647
+ if queue_manager
648
+ puts "🔥 QUEUE: Processing queued submissions first"
649
+ queue_manager.process_pending_submissions_now
650
+ end
651
+ end
652
+
653
+ private
654
+
655
+ # Get the HTTP client instance
656
+ #
657
+ # @return [ComplyanceSDK::HTTP::Client] The HTTP client
658
+ def http_client
659
+ @http_client ||= ComplyanceSDK::HTTP::Client.new(configuration)
660
+ end
661
+
662
+ # Get the default source from configuration
663
+ #
664
+ # @return [ComplyanceSDK::Models::Source, nil] The default source
665
+ def default_source
666
+ configuration.sources.first
667
+ end
668
+
669
+ # Find source type by name and version from configuration
670
+ #
671
+ # @param name [String] The source name
672
+ # @param version [String] The source version
673
+ # @return [Symbol] The source type (defaults to :first_party)
674
+ def find_source_type(name, version)
675
+ configured_source = configuration.sources.find do |source|
676
+ source.name == name && source.version == version
677
+ end
678
+
679
+ configured_source&.type || :first_party
680
+ end
681
+
682
+ # Get the queue manager instance
683
+ #
684
+ # @return [ComplyanceSDK::Queue::PersistentQueueManager, nil] The queue manager
685
+ def queue_manager
686
+ return nil unless configuration
687
+
688
+ @queue_manager ||= ComplyanceSDK::Queue::PersistentQueueManager.new(
689
+ configuration.api_key,
690
+ configuration.environment == ComplyanceSDK::Models::Environment::LOCAL
691
+ )
692
+ end
693
+
694
+ # Validate required parameter
695
+ #
696
+ # @param value [Object] The value to validate
697
+ # @param field [Symbol] The field name
698
+ # @param message [String] The error message
699
+ def validate_required_parameter(value, field, message)
700
+ if value.nil?
701
+ raise ComplyanceSDK::Exceptions::ValidationError.new(
702
+ message,
703
+ context: { field: field }
704
+ )
705
+ end
706
+ end
707
+
708
+ # Validate country restrictions based on current environment
709
+ #
710
+ # @param country [Symbol] The country
711
+ # @param environment [Symbol] The environment
712
+ def validate_country_for_environment(country, environment)
713
+ unless ComplyanceSDK::Models::Country.allowed_for_environment?(country, environment)
714
+ error_message = ComplyanceSDK::Models::Country.validation_error_message(country, environment)
715
+ raise ComplyanceSDK::Exceptions::ValidationError.new(
716
+ "Country not allowed for environment: #{error_message}",
717
+ context: { country: country, environment: environment }
718
+ )
719
+ end
720
+ end
721
+
722
+ # Deep merge meta.config flags into payload
723
+ # User values take precedence over policy defaults
724
+ #
725
+ # @param payload [Hash] The original payload
726
+ # @param config_flags [Hash] The config flags to merge
727
+ # @return [Hash] The merged payload
728
+ def deep_merge_into_meta_config(payload, config_flags)
729
+ merged = payload.dup
730
+
731
+ meta = merged[:meta] || merged['meta'] || {}
732
+ config = meta[:config] || meta['config'] || {}
733
+
734
+ # Merge config flags (user values take precedence)
735
+ merged_config = config_flags.merge(config)
736
+
737
+ meta[:config] = merged_config
738
+ merged[:meta] = meta
739
+
740
+ merged
741
+ end
742
+
743
+ # Auto-set invoice_data.document_type based on LogicalDocType
744
+ #
745
+ # @param payload [Hash] The payload to modify
746
+ # @param logical_type [Symbol] The logical document type
747
+ def set_invoice_data_document_type(payload, logical_type)
748
+ return unless payload
749
+
750
+ invoice_data = payload[:invoice_data] || payload['invoice_data']
751
+ return unless invoice_data
752
+
753
+ # Determine document type string based on LogicalDocType
754
+ document_type = ComplyanceSDK::Models::LogicalDocType.invoice_data_document_type(logical_type)
755
+
756
+ # Set the document_type field (use string key for consistency)
757
+ invoice_data['document_type'] = document_type
758
+ end
759
+
760
+ # Generate default destinations for a country and document type
761
+ #
762
+ # @param country [Symbol] The country
763
+ # @param document_type [String] The document type
764
+ # @return [Array] Array of destinations
765
+ def generate_default_destinations(country, document_type)
766
+ destinations = []
767
+
768
+ # Auto-generate tax authority destination
769
+ authority = ComplyanceSDK::Models::Country.default_tax_authority(country)
770
+ if authority
771
+ destinations << {
772
+ type: :tax_authority,
773
+ country: country.to_s.upcase,
774
+ authority: authority,
775
+ document_type: document_type
776
+ }
777
+ end
778
+
779
+ destinations
780
+ end
781
+
782
+ # Internal method to push to Unify API with custom document type string
783
+ #
784
+ # @param source_ref [ComplyanceSDK::Models::SourceRef] The source reference
785
+ # @param base_document_type [Symbol] The base document type
786
+ # @param document_type_string [String] The custom document type string
787
+ # @param country [Symbol] The country
788
+ # @param operation [Symbol] The operation
789
+ # @param mode [Symbol] The mode
790
+ # @param purpose [Symbol] The purpose
791
+ # @param payload [Hash] The payload
792
+ # @param destinations [Array] The destinations
793
+ # @return [ComplyanceSDK::Models::UnifyResponse] The response
794
+ def push_to_unify_internal_with_document_type(source_ref, base_document_type, document_type_string, country, operation, mode, purpose, payload, destinations)
795
+ # Create UnifyRequest using the model
796
+ request = ComplyanceSDK::Models::UnifyRequest.new(
797
+ source: source_ref,
798
+ document_type: base_document_type,
799
+ country: country,
800
+ operation: operation,
801
+ mode: mode,
802
+ purpose: purpose,
803
+ payload: payload,
804
+ destinations: destinations,
805
+ metadata: {
806
+ api_key: configuration.api_key,
807
+ request_id: "req_#{Time.now.to_i}_#{rand}",
808
+ timestamp: Time.now.utc.strftime('%Y-%m-%dT%H:%M:%SZ'),
809
+ environment: ComplyanceSDK::Models::Environment.to_api_value(configuration.environment),
810
+ correlation_id: configuration.correlation_id
811
+ }
812
+ )
813
+
814
+ # Convert to hash for API call
815
+ request_data = request.to_h
816
+
817
+ begin
818
+ # Use circuit breaker if configured
819
+ if configuration.retry_config&.circuit_breaker_enabled?
820
+ circuit_breaker = ComplyanceSDK::Retry::CircuitBreaker.new(
821
+ configuration.retry_config.circuit_breaker_config
822
+ )
823
+
824
+ return circuit_breaker.execute do
825
+ http_client.post('', request_data)
826
+ end
827
+ else
828
+ return http_client.post('', request_data)
829
+ end
830
+ rescue ComplyanceSDK::Exceptions::CircuitBreakerOpenError => e
831
+ # Circuit breaker is open - queue the request
832
+ puts "🚫 Circuit breaker is OPEN - queuing request for retry"
833
+
834
+ if queue_manager
835
+ complete_request_json = JSON.generate(request_data)
836
+ submission = {
837
+ payload: complete_request_json,
838
+ source: { id: source_ref.identity },
839
+ country: country,
840
+ document_type: base_document_type
841
+ }
842
+
843
+ queue_manager.enqueue(submission)
844
+
845
+ return {
846
+ status: 'queued',
847
+ message: "Circuit breaker is open - request queued for retry. Submission ID: #{request_data[:request_id]}",
848
+ data: {
849
+ submission: {
850
+ submission_id: request_data[:request_id]
851
+ }
852
+ }
853
+ }
854
+ else
855
+ raise e
856
+ end
857
+ rescue ComplyanceSDK::Exceptions::SDKException => e
858
+ puts "🔥 QUEUE: SDKException caught - Error: #{e.message}, ServerError: #{server_error?(e)}, QueueManager: #{!queue_manager.nil?}"
859
+
860
+ # Check if the error is a 500-range server error and queue is enabled
861
+ if server_error?(e) && queue_manager
862
+ # Store the complete UnifyRequest as JSON to maintain exact API format
863
+ complete_request_json = JSON.generate(request_data)
864
+ puts "🔥 QUEUE: Successfully converted complete UnifyRequest to JSON with length: #{complete_request_json.length}"
865
+ puts "🔥 QUEUE: Complete request JSON preview: #{complete_request_json[0..199]}"
866
+
867
+ # Create a submission for the queue
868
+ submission = {
869
+ payload: complete_request_json,
870
+ source: { id: source_ref.identity },
871
+ country: country,
872
+ document_type: base_document_type
873
+ }
874
+
875
+ puts "🔥 QUEUE: Created submission with complete request length: #{submission[:payload].length}"
876
+
877
+ # Enqueue the failed submission for background retry
878
+ queue_manager.enqueue(submission)
879
+
880
+ # Return a response indicating the submission was queued
881
+ return {
882
+ status: 'queued',
883
+ message: "Request failed but has been queued for retry. Submission ID: #{request_data[:request_id]}",
884
+ data: {
885
+ submission: {
886
+ submission_id: request_data[:request_id]
887
+ }
888
+ }
889
+ }
890
+ end
891
+
892
+ # If not a server error or queue not available, re-throw the exception
893
+ raise e
894
+ end
895
+ end
896
+
897
+ # Determines if an SDK exception represents a server error (500-range HTTP status codes).
898
+ # Only 500-range errors (500-599) should trigger queue access.
899
+ #
900
+ # @param exception [ComplyanceSDK::Exceptions::SDKException] The SDK exception
901
+ # @return [Boolean] True if it's a 500-range HTTP error; otherwise, false
902
+ def server_error?(exception)
903
+ return false unless exception.context
904
+
905
+ # Check HTTP status code in context
906
+ http_status_obj = exception.context[:http_status] || exception.context['httpStatus']
907
+ if http_status_obj
908
+ begin
909
+ status_code = http_status_obj.to_i
910
+
911
+ # Only 500-range errors (500-599) should trigger queue access
912
+ is_server_status = status_code >= 500 && status_code < 600
913
+ if !is_server_status
914
+ puts "HTTP status #{status_code} detected (non 500-range) - skipping queue"
915
+ else
916
+ puts "Server error detected from HTTP status: #{status_code}"
917
+ end
918
+ return is_server_status
919
+ rescue StandardError => ex
920
+ puts "Invalid HTTP status format: #{http_status_obj}"
921
+ # Ignore invalid status
922
+ end
923
+ else
924
+ puts "No httpStatus in exception context, not counting as server error"
925
+ end
926
+
927
+ # Fallback: use error codes only when HTTP status is unavailable
928
+ error_code = exception.context[:error_code] || exception.context['error_code']
929
+ return error_code == 'INTERNAL_SERVER_ERROR' || error_code == 'SERVICE_UNAVAILABLE'
930
+ end
931
+ end
932
+ end
933
+
934
+ # Rails integration - removed railtie
935
+ # require "complyance_sdk/railtie" if defined?(Rails)