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,165 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ComplyanceSDK
4
+ module Jobs
5
+ # Sidekiq-specific job implementation
6
+ class SidekiqJob
7
+ # Include Sidekiq::Job if available
8
+ if defined?(Sidekiq)
9
+ include Sidekiq::Job
10
+
11
+ # Configure Sidekiq options
12
+ sidekiq_options queue: :complyance_sdk,
13
+ retry: 3,
14
+ backtrace: true,
15
+ dead: false
16
+
17
+ # Custom retry logic for Sidekiq
18
+ sidekiq_retry_in do |count, exception|
19
+ case exception
20
+ when ComplyanceSDK::Exceptions::NetworkError
21
+ # Exponential backoff for network errors
22
+ (count ** 2) + 15 + (rand(30) * (count + 1))
23
+ when ComplyanceSDK::Exceptions::APIError
24
+ # Only retry certain API errors
25
+ if exception.status_code && [429, 500, 502, 503, 504].include?(exception.status_code)
26
+ (count ** 2) + 10 + (rand(10) * (count + 1))
27
+ else
28
+ :kill # Don't retry other API errors
29
+ end
30
+ when ComplyanceSDK::Exceptions::ConfigurationError,
31
+ ComplyanceSDK::Exceptions::ValidationError
32
+ :kill # Don't retry configuration or validation errors
33
+ else
34
+ # Default exponential backoff
35
+ (count ** 2) + 15 + (rand(30) * (count + 1))
36
+ end
37
+ end
38
+ end
39
+
40
+ # Process a document using Sidekiq
41
+ #
42
+ # @param request_data [Hash] The UnifyRequest data
43
+ # @param callback_url [String, nil] Optional callback URL for results
44
+ # @param callback_headers [Hash] Optional headers for callback
45
+ def perform(request_data, callback_url = nil, callback_headers = {})
46
+ unless ComplyanceSDK.configured?
47
+ raise ComplyanceSDK::Exceptions::ConfigurationError.new(
48
+ "ComplyanceSDK is not configured for Sidekiq jobs"
49
+ )
50
+ end
51
+
52
+ client = ComplyanceSDK::HTTP::Client.new(ComplyanceSDK.configuration)
53
+
54
+ log_info("Starting Sidekiq document processing", {
55
+ document_type: request_data["document_type"],
56
+ country: request_data["country"],
57
+ jid: jid
58
+ })
59
+
60
+ begin
61
+ response = client.post("/api/v1/unify", request_data)
62
+
63
+ log_info("Sidekiq document processed successfully", {
64
+ response_status: response["status"],
65
+ submission_id: response.dig("data", "submission_id"),
66
+ jid: jid
67
+ })
68
+
69
+ # Send callback if provided
70
+ if callback_url
71
+ send_callback(callback_url, callback_headers, {
72
+ status: "success",
73
+ data: response,
74
+ request_id: request_data["metadata"]&.dig("request_id"),
75
+ job_id: jid
76
+ })
77
+ end
78
+
79
+ response
80
+ rescue => error
81
+ log_error("Sidekiq document processing failed", {
82
+ error_class: error.class.name,
83
+ error_message: error.message,
84
+ jid: jid
85
+ })
86
+
87
+ # Send error callback if provided
88
+ if callback_url
89
+ send_callback(callback_url, callback_headers, {
90
+ status: "error",
91
+ error: {
92
+ code: error.respond_to?(:code) ? error.code : "unknown_error",
93
+ message: error.message
94
+ },
95
+ request_id: request_data["metadata"]&.dig("request_id"),
96
+ job_id: jid
97
+ })
98
+ end
99
+
100
+ raise error
101
+ end
102
+ end
103
+
104
+ private
105
+
106
+ def jid
107
+ @jid ||= defined?(Sidekiq) ? (bid || "unknown") : "no-sidekiq"
108
+ end
109
+
110
+ def send_callback(url, headers, payload)
111
+ require "net/http"
112
+ require "uri"
113
+ require "json"
114
+
115
+ uri = URI(url)
116
+ http = Net::HTTP.new(uri.host, uri.port)
117
+ http.use_ssl = uri.scheme == "https"
118
+
119
+ request = Net::HTTP::Post.new(uri)
120
+ request["Content-Type"] = "application/json"
121
+ headers.each { |key, value| request[key] = value }
122
+ request.body = payload.to_json
123
+
124
+ response = http.request(request)
125
+
126
+ log_info("Sidekiq callback sent", {
127
+ callback_url: url,
128
+ callback_status: response.code,
129
+ payload_status: payload[:status],
130
+ jid: jid
131
+ })
132
+ rescue => error
133
+ log_warn("Sidekiq callback failed", {
134
+ callback_url: url,
135
+ error: error.message,
136
+ jid: jid
137
+ })
138
+ end
139
+
140
+ def log_info(message, context = {})
141
+ log_message("INFO", message, context)
142
+ end
143
+
144
+ def log_warn(message, context = {})
145
+ log_message("WARN", message, context)
146
+ end
147
+
148
+ def log_error(message, context = {})
149
+ log_message("ERROR", message, context)
150
+ end
151
+
152
+ def log_message(level, message, context = {})
153
+ if defined?(Sidekiq) && Sidekiq.logger
154
+ full_message = "ComplyanceSDK Sidekiq Job: #{message}"
155
+ full_message += " Context: #{context}" unless context.empty?
156
+ Sidekiq.logger.send(level.downcase.to_sym, full_message)
157
+ elsif defined?(Rails) && Rails.logger
158
+ full_message = "ComplyanceSDK Sidekiq Job: #{message}"
159
+ full_message += " Context: #{context}" unless context.empty?
160
+ Rails.logger.send(level.downcase.to_sym, full_message)
161
+ end
162
+ end
163
+ end
164
+ end
165
+ end
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ComplyanceSDK
4
+ module Middleware
5
+ # Rack middleware for ComplyanceSDK integration
6
+ class RackMiddleware
7
+ def initialize(app, options = {})
8
+ @app = app
9
+ @options = options
10
+ end
11
+
12
+ def call(env)
13
+ # Add ComplyanceSDK context to the request
14
+ env["complyance_sdk.configured"] = ComplyanceSDK.configured?
15
+ env["complyance_sdk.environment"] = ComplyanceSDK.configuration&.environment
16
+
17
+ # Add request ID for tracing
18
+ env["complyance_sdk.request_id"] = generate_request_id
19
+
20
+ status, headers, response = @app.call(env)
21
+
22
+ # Add ComplyanceSDK headers to response if configured
23
+ if ComplyanceSDK.configured? && @options[:add_headers]
24
+ headers["X-ComplyanceSDK-Version"] = ComplyanceSDK::VERSION
25
+ headers["X-ComplyanceSDK-Environment"] = ComplyanceSDK.configuration.environment.to_s
26
+ end
27
+
28
+ [status, headers, response]
29
+ end
30
+
31
+ private
32
+
33
+ def generate_request_id
34
+ require "securerandom"
35
+ SecureRandom.uuid
36
+ end
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,205 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ComplyanceSDK
4
+ module Models
5
+ # Country enumeration for supported countries
6
+ class Country
7
+ # Supported countries
8
+ SA = :sa # Saudi Arabia
9
+ MY = :my # Malaysia
10
+ AE = :ae # United Arab Emirates
11
+ SG = :sg # Singapore
12
+ EG = :eg # Egypt
13
+ IN = :in # India
14
+ PH = :ph # Philippines
15
+ TH = :th # Thailand
16
+ VN = :vn # Vietnam
17
+ ID = :id # Indonesia
18
+ BD = :bd # Bangladesh
19
+ LK = :lk # Sri Lanka
20
+ PK = :pk # Pakistan
21
+ NP = :np # Nepal
22
+ MM = :mm # Myanmar
23
+ KH = :kh # Cambodia
24
+ LA = :la # Laos
25
+ BN = :bn # Brunei
26
+ MV = :mv # Maldives
27
+ BT = :bt # Bhutan
28
+
29
+ # All supported countries
30
+ ALL_COUNTRIES = [
31
+ SA, MY, AE, SG, EG, IN, PH, TH, VN, ID,
32
+ BD, LK, PK, NP, MM, KH, LA, BN, MV, BT
33
+ ].freeze
34
+
35
+ class << self
36
+ # Check if a country is valid
37
+ #
38
+ # @param country [Symbol] The country code to validate
39
+ # @return [Boolean] True if valid, false otherwise
40
+ def valid?(country)
41
+ ALL_COUNTRIES.include?(country)
42
+ end
43
+
44
+ # Parse a string to a country symbol
45
+ #
46
+ # @param str [String] The string to parse
47
+ # @return [Symbol] The country symbol
48
+ # @raise [ArgumentError] If the string is invalid
49
+ def from_string(str)
50
+ return str if str.is_a?(Symbol)
51
+
52
+ country = str.to_s.downcase.to_sym
53
+
54
+ unless valid?(country)
55
+ raise ArgumentError, "Invalid country: #{str}. Valid countries: #{ALL_COUNTRIES.join(', ')}"
56
+ end
57
+
58
+ country
59
+ end
60
+
61
+ # Convert a country to string format
62
+ #
63
+ # @param country [Symbol] The country
64
+ # @return [String] The string representation
65
+ def to_string(country)
66
+ country.to_s.upcase
67
+ end
68
+
69
+ # Get the default tax authority for a country
70
+ #
71
+ # @param country [Symbol] The country
72
+ # @return [String, nil] The tax authority name or nil if not mapped
73
+ def default_tax_authority(country)
74
+ case country
75
+ when SA
76
+ 'ZATCA'
77
+ when MY
78
+ 'LHDN'
79
+ when AE
80
+ 'FTA'
81
+ when SG
82
+ 'IRAS'
83
+ when EG
84
+ 'ETA'
85
+ when IN
86
+ 'GSTN'
87
+ when PH
88
+ 'BIR'
89
+ when TH
90
+ 'RD'
91
+ when VN
92
+ 'GDT'
93
+ when ID
94
+ 'DJP'
95
+ else
96
+ nil
97
+ end
98
+ end
99
+
100
+ # Get the full country name
101
+ #
102
+ # @param country [Symbol] The country code
103
+ # @return [String] The full country name
104
+ def full_name(country)
105
+ case country
106
+ when SA
107
+ 'Saudi Arabia'
108
+ when MY
109
+ 'Malaysia'
110
+ when AE
111
+ 'United Arab Emirates'
112
+ when SG
113
+ 'Singapore'
114
+ when EG
115
+ 'Egypt'
116
+ when IN
117
+ 'India'
118
+ when PH
119
+ 'Philippines'
120
+ when TH
121
+ 'Thailand'
122
+ when VN
123
+ 'Vietnam'
124
+ when ID
125
+ 'Indonesia'
126
+ when BD
127
+ 'Bangladesh'
128
+ when LK
129
+ 'Sri Lanka'
130
+ when PK
131
+ 'Pakistan'
132
+ when NP
133
+ 'Nepal'
134
+ when MM
135
+ 'Myanmar'
136
+ when KH
137
+ 'Cambodia'
138
+ when LA
139
+ 'Laos'
140
+ when BN
141
+ 'Brunei'
142
+ when MV
143
+ 'Maldives'
144
+ when BT
145
+ 'Bhutan'
146
+ else
147
+ country.to_s.upcase
148
+ end
149
+ end
150
+
151
+ # Check if country is allowed for production environments
152
+ # Based on the Java SDK's validation rules
153
+ #
154
+ # @param country [Symbol] The country
155
+ # @param environment [Symbol] The environment
156
+ # @return [Boolean] True if allowed
157
+ def allowed_for_environment?(country, environment)
158
+ case environment
159
+ when :dev, :test, :stage, :local
160
+ # Development environments allow all countries
161
+ true
162
+ when :sandbox, :simulation, :production
163
+ # Production environments have restrictions
164
+ case country
165
+ when SA
166
+ # SA is allowed in all production environments
167
+ true
168
+ when MY
169
+ # MY is allowed in SANDBOX and PRODUCTION only (not SIMULATION)
170
+ environment != :simulation
171
+ else
172
+ # Other countries are blocked in production environments
173
+ false
174
+ end
175
+ else
176
+ # Unknown environment, default to allowing
177
+ true
178
+ end
179
+ end
180
+
181
+ # Get validation error message for country/environment combination
182
+ #
183
+ # @param country [Symbol] The country
184
+ # @param environment [Symbol] The environment
185
+ # @return [String, nil] Error message or nil if allowed
186
+ def validation_error_message(country, environment)
187
+ return nil if allowed_for_environment?(country, environment)
188
+
189
+ case environment
190
+ when :simulation
191
+ if country == MY
192
+ "MY (Malaysia) is not allowed in SIMULATION environment. Use SANDBOX or PRODUCTION."
193
+ else
194
+ "Only SA and MY are allowed for #{environment.to_s.upcase}. Use DEV/TEST/STAGE for other countries."
195
+ end
196
+ when :sandbox, :production
197
+ "Only SA and MY are allowed for #{environment.to_s.upcase}. Use DEV/TEST/STAGE for other countries."
198
+ else
199
+ "Country #{country.to_s.upcase} is not allowed for environment #{environment.to_s.upcase}."
200
+ end
201
+ end
202
+ end
203
+ end
204
+ end
205
+ end
@@ -0,0 +1,159 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'logical_doc_type'
4
+ require_relative 'policy_result'
5
+ require_relative 'document_type'
6
+ require_relative 'country'
7
+
8
+ module ComplyanceSDK
9
+ module Models
10
+ # Registry for country-specific document type policies
11
+ # Maps logical document types to base document types and meta configuration flags
12
+ class CountryPolicyRegistry
13
+ class << self
14
+ # Evaluate a logical document type for a specific country
15
+ #
16
+ # @param country [Symbol] The country code
17
+ # @param logical_type [Symbol] The logical document type
18
+ # @return [PolicyResult] The policy result
19
+ # @raise [ComplyanceSDK::Exceptions::ValidationError] If the combination is not allowed
20
+ def evaluate(country, logical_type)
21
+ # Validate inputs
22
+ unless Country.valid?(country)
23
+ raise ComplyanceSDK::Exceptions::ValidationError.new(
24
+ "Invalid country: #{country}. Valid countries: #{Country::ALL_COUNTRIES.join(', ')}"
25
+ )
26
+ end
27
+
28
+ unless LogicalDocType.valid?(logical_type)
29
+ raise ComplyanceSDK::Exceptions::ValidationError.new(
30
+ "Invalid logical document type: #{logical_type}. Valid types: #{LogicalDocType::ALL_TYPES.join(', ')}"
31
+ )
32
+ end
33
+
34
+ # Get policy for the logical type (country-agnostic for now)
35
+ policy = create_policy_for_logical_type(logical_type)
36
+
37
+ if policy.nil?
38
+ raise ComplyanceSDK::Exceptions::ValidationError.new(
39
+ "Document type not allowed for country: #{country}. Logical type: #{logical_type}"
40
+ )
41
+ end
42
+
43
+ policy
44
+ end
45
+
46
+ private
47
+
48
+ # Create meta configuration flags map
49
+ #
50
+ # @param is_export [Boolean] Export flag
51
+ # @param is_self_billed [Boolean] Self-billed flag
52
+ # @param is_third_party [Boolean] Third party flag
53
+ # @param is_nominal_supply [Boolean] Nominal supply flag
54
+ # @param is_summary [Boolean] Summary flag
55
+ # @param is_b2b [Boolean] B2B flag
56
+ # @param is_prepayment [Boolean] Prepayment flag
57
+ # @param is_adjusted [Boolean] Adjusted flag
58
+ # @param is_receipt [Boolean] Receipt flag
59
+ # @return [Hash] The configuration flags
60
+ def create_config_map(is_export: false, is_self_billed: false, is_third_party: false,
61
+ is_nominal_supply: false, is_summary: false, is_b2b: false,
62
+ is_prepayment: false, is_adjusted: false, is_receipt: false)
63
+ {
64
+ isExport: is_export,
65
+ isSelfBilled: is_self_billed,
66
+ isThirdParty: is_third_party,
67
+ isNominal: is_nominal_supply, # Changed from isNominalSupply to isNominal to match Java
68
+ isSummary: is_summary,
69
+ isB2B: is_b2b,
70
+ isPrepayment: is_prepayment,
71
+ isAdjusted: is_adjusted,
72
+ isReceipt: is_receipt
73
+ }
74
+ end
75
+
76
+ # Get document type string for a logical type
77
+ #
78
+ # @param logical_type [Symbol] The logical document type
79
+ # @return [String] The document type string
80
+ def get_document_type_string(logical_type)
81
+ LogicalDocType.invoice_data_document_type(logical_type)
82
+ end
83
+
84
+ # Get meta config document type for a logical type
85
+ #
86
+ # @param logical_type [Symbol] The logical document type
87
+ # @return [String] The meta config document type
88
+ def get_meta_config_document_type(logical_type)
89
+ LogicalDocType.meta_config_document_type(logical_type)
90
+ end
91
+
92
+ # Create policy result for a logical document type
93
+ #
94
+ # @param logical_type [Symbol] The logical document type
95
+ # @return [PolicyResult] The policy result
96
+ def create_policy_for_logical_type(logical_type)
97
+ document_type = get_document_type_string(logical_type)
98
+ type_name = logical_type.to_s
99
+
100
+ # Determine B2B vs B2C based on TAX_INVOICE vs SIMPLIFIED_TAX_INVOICE
101
+ is_b2b = if type_name.start_with?('simplified_tax_invoice')
102
+ false # B2C
103
+ elsif type_name.start_with?('tax_invoice')
104
+ true # B2B
105
+ else
106
+ # For legacy types, set appropriate defaults
107
+ # Legacy types - determine B2B based on context
108
+ # case logical_type
109
+ # when LogicalDocType::INVOICE,
110
+ # LogicalDocType::EXPORT_INVOICE,
111
+ # LogicalDocType::EXPORT_THIRD_PARTY_INVOICE,
112
+ # LogicalDocType::THIRD_PARTY_INVOICE,
113
+ # LogicalDocType::SELF_BILLED_INVOICE,
114
+ # LogicalDocType::NOMINAL_SUPPLY_INVOICE,
115
+ # LogicalDocType::SUMMARY_INVOICE
116
+ # true
117
+ # else
118
+ # false
119
+ # end
120
+ true # Default for non-simplified types
121
+ end
122
+
123
+ # Determine other flags based on type name
124
+ is_export = type_name.include?('export')
125
+ is_self_billed = type_name.include?('self_billed')
126
+ is_third_party = type_name.include?('third_party')
127
+ is_nominal_supply = type_name.include?('nominal_supply')
128
+ is_summary = type_name.include?('summary')
129
+ is_prepayment = type_name.include?('prepayment')
130
+ is_adjusted = type_name.include?('adjusted')
131
+ is_receipt = type_name.include?('receipt')
132
+
133
+ config = create_config_map(
134
+ is_export: is_export,
135
+ is_self_billed: is_self_billed,
136
+ is_third_party: is_third_party,
137
+ is_nominal_supply: is_nominal_supply,
138
+ is_summary: is_summary,
139
+ is_b2b: is_b2b,
140
+ is_prepayment: is_prepayment,
141
+ is_adjusted: is_adjusted,
142
+ is_receipt: is_receipt
143
+ )
144
+
145
+ # Determine base DocumentType
146
+ base_type = if type_name.include?('credit_note')
147
+ DocumentType::CREDIT_NOTE
148
+ elsif type_name.include?('debit_note')
149
+ DocumentType::DEBIT_NOTE
150
+ else
151
+ DocumentType::TAX_INVOICE
152
+ end
153
+
154
+ PolicyResult.new(base_type, config, document_type)
155
+ end
156
+ end
157
+ end
158
+ end
159
+ end
@@ -0,0 +1,52 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ComplyanceSDK
4
+ module Models
5
+ # Document type enumeration
6
+ module DocumentType
7
+ # Tax invoice document type
8
+ TAX_INVOICE = :tax_invoice
9
+
10
+ # Credit note document type
11
+ CREDIT_NOTE = :credit_note
12
+
13
+ # Debit note document type
14
+ DEBIT_NOTE = :debit_note
15
+
16
+ # Get all valid document types
17
+ #
18
+ # @return [Array<Symbol>] All valid document types
19
+ def self.all
20
+ [TAX_INVOICE, CREDIT_NOTE, DEBIT_NOTE]
21
+ end
22
+
23
+ # Check if a document type is valid
24
+ #
25
+ # @param document_type [Symbol, String] The document type to check
26
+ # @return [Boolean] True if valid, false otherwise
27
+ def self.valid?(document_type)
28
+ return false if document_type.nil?
29
+ all.include?(document_type.to_sym)
30
+ end
31
+
32
+ # Convert string to document type symbol
33
+ #
34
+ # @param document_type [String, Symbol] The document type
35
+ # @return [Symbol, nil] The document type symbol or nil if invalid
36
+ def self.normalize(document_type)
37
+ return nil if document_type.nil?
38
+
39
+ case document_type.to_s.downcase.gsub(/[-_\s]/, '')
40
+ when "taxinvoice", "invoice"
41
+ TAX_INVOICE
42
+ when "creditnote", "credit"
43
+ CREDIT_NOTE
44
+ when "debitnote", "debit"
45
+ DEBIT_NOTE
46
+ else
47
+ document_type.to_sym if valid?(document_type)
48
+ end
49
+ end
50
+ end
51
+ end
52
+ end