br-utils 0.1.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 (40) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +120 -0
  3. data/.rspec +2 -0
  4. data/.travis.yml +5 -0
  5. data/Gemfile +3 -0
  6. data/LICENSE.txt +21 -0
  7. data/README.md +348 -0
  8. data/Rakefile +6 -0
  9. data/examples/boleto_usage_example.rb +79 -0
  10. data/examples/cep_usage_example.rb +148 -0
  11. data/examples/cnh_usage_example.rb +120 -0
  12. data/examples/cnpj_usage_example.rb +227 -0
  13. data/examples/cpf_usage_example.rb +237 -0
  14. data/examples/currency_usage_example.rb +266 -0
  15. data/examples/date_usage_example.rb +259 -0
  16. data/examples/email_usage_example.rb +321 -0
  17. data/examples/legal_nature_usage_example.rb +437 -0
  18. data/examples/legal_process_usage_example.rb +444 -0
  19. data/examples/license_plate_usage_example.rb +440 -0
  20. data/examples/phone_usage_example.rb +595 -0
  21. data/examples/pis_usage_example.rb +588 -0
  22. data/examples/renavam_usage_example.rb +499 -0
  23. data/examples/voter_id_usage_example.rb +573 -0
  24. data/lib/brazilian-utils/boleto-utils.rb +176 -0
  25. data/lib/brazilian-utils/cep-utils.rb +330 -0
  26. data/lib/brazilian-utils/cnh-utils.rb +88 -0
  27. data/lib/brazilian-utils/cnpj-utils.rb +202 -0
  28. data/lib/brazilian-utils/cpf-utils.rb +192 -0
  29. data/lib/brazilian-utils/currency-utils.rb +226 -0
  30. data/lib/brazilian-utils/data/legal_process_ids.json +38 -0
  31. data/lib/brazilian-utils/date-utils.rb +244 -0
  32. data/lib/brazilian-utils/email-utils.rb +54 -0
  33. data/lib/brazilian-utils/legal-nature-utils.rb +235 -0
  34. data/lib/brazilian-utils/legal-process-utils.rb +240 -0
  35. data/lib/brazilian-utils/license-plate-utils.rb +279 -0
  36. data/lib/brazilian-utils/phone-utils.rb +272 -0
  37. data/lib/brazilian-utils/pis-utils.rb +151 -0
  38. data/lib/brazilian-utils/renavam-utils.rb +113 -0
  39. data/lib/brazilian-utils/voter-id-utils.rb +165 -0
  40. metadata +123 -0
@@ -0,0 +1,176 @@
1
+ # frozen_string_literal: true
2
+
3
+ module BrazilianUtils
4
+ module BoletoUtils
5
+ # Every Digitable Line from Boleto has exactly 47 characters
6
+ DIGITABLE_LINE_LENGTH = 47
7
+
8
+ # Positions to convert digitable line to boleto
9
+ DIGITABLE_LINE_TO_BOLETO_CONVERT_POSITIONS = [
10
+ { start: 0, end: 4 },
11
+ { start: 32, end: 47 },
12
+ { start: 4, end: 9 },
13
+ { start: 10, end: 20 },
14
+ { start: 21, end: 31 }
15
+ ].freeze
16
+
17
+ # Partials to verify with mod10
18
+ PARTIALS_TO_VERIFY_MOD10 = [
19
+ { start: 0, end: 9, digit_index: 9 },
20
+ { start: 10, end: 20, digit_index: 20 },
21
+ { start: 21, end: 31, digit_index: 31 }
22
+ ].freeze
23
+
24
+ # Mod10 weights
25
+ MOD10_WEIGHTS = [2, 1].freeze
26
+
27
+ # Check digit mod11 position
28
+ CHECK_DIGIT_MOD11_POSITION = 4
29
+
30
+ # Mod11 weights configuration
31
+ MOD11_WEIGHTS = {
32
+ initial: 2,
33
+ end: 9,
34
+ increment: 1
35
+ }.freeze
36
+
37
+ class << self
38
+ # Validates if a given Digitable Line is valid.
39
+ #
40
+ # @param digitable_line [String] The boleto digitable line to validate
41
+ # @return [Boolean] true if valid, false otherwise
42
+ def is_valid(digitable_line)
43
+ # Extract only numbers from the input
44
+ digitable_line_numbers = extract_only_numbers(digitable_line)
45
+
46
+ return false unless valid_length?(digitable_line_numbers)
47
+ return false unless validate_digitable_line_partials(digitable_line_numbers)
48
+
49
+ validate_mod11_check_digit(digitable_line_numbers)
50
+ end
51
+
52
+ # Alias for is_valid
53
+ alias valid? is_valid
54
+
55
+ private
56
+
57
+ # Extract only numeric characters from a string.
58
+ #
59
+ # @param str [String] The input string
60
+ # @return [String] String containing only numeric characters
61
+ def extract_only_numbers(str)
62
+ return '' if str.nil?
63
+
64
+ str.gsub(/\D/, '')
65
+ end
66
+
67
+ # Validates the string length.
68
+ #
69
+ # @param digitable_line [String] The digitable line to check
70
+ # @return [Boolean] true if length is exactly 47, false otherwise
71
+ def valid_length?(digitable_line)
72
+ digitable_line.length == DIGITABLE_LINE_LENGTH
73
+ end
74
+
75
+ # Validates the digitable line partials using mod10.
76
+ #
77
+ # @param digitable_line [String] The digitable line to validate
78
+ # @return [Boolean] true if all partials are valid, false otherwise
79
+ def validate_digitable_line_partials(digitable_line)
80
+ PARTIALS_TO_VERIFY_MOD10.all? do |partial|
81
+ partial_str = digitable_line[partial[:start]...partial[:end]]
82
+ mod10 = get_mod10(partial_str)
83
+ digit = digitable_line[partial[:digit_index]].to_i
84
+ digit == mod10
85
+ end
86
+ end
87
+
88
+ # Calculate mod10 for a given partial string.
89
+ #
90
+ # @param partial [String] The partial string to calculate mod10 for
91
+ # @return [Integer] The mod10 check digit
92
+ def get_mod10(partial)
93
+ sum = 0
94
+ partial_reversed = partial.reverse
95
+
96
+ partial_reversed.each_char.with_index do |char, index|
97
+ partial_value = char.to_i
98
+ weight = MOD10_WEIGHTS[index % 2]
99
+ multiplier = partial_value * weight
100
+
101
+ if multiplier > 9
102
+ sum += 1 + (multiplier % 10)
103
+ else
104
+ sum += multiplier
105
+ end
106
+ end
107
+
108
+ mod10 = sum % 10
109
+ if mod10 > 0
110
+ 10 - mod10
111
+ else
112
+ 0
113
+ end
114
+ end
115
+
116
+ # Validates the mod11 check digit.
117
+ #
118
+ # @param digitable_line [String] The digitable line to validate
119
+ # @return [Boolean] true if mod11 check digit is valid, false otherwise
120
+ def validate_mod11_check_digit(digitable_line)
121
+ parsed_digitable_line = parse_digitable_line(digitable_line)
122
+
123
+ # Extract the value before and after the check digit position
124
+ value = parsed_digitable_line[0...CHECK_DIGIT_MOD11_POSITION] +
125
+ parsed_digitable_line[(CHECK_DIGIT_MOD11_POSITION + 1)..-1]
126
+
127
+ mod11 = get_mod11(value)
128
+ mod11_value = parsed_digitable_line[CHECK_DIGIT_MOD11_POSITION].to_i
129
+
130
+ mod11_value == mod11
131
+ end
132
+
133
+ # Parse the digitable line by extracting specific positions.
134
+ #
135
+ # @param digitable_line [String] The digitable line to parse
136
+ # @return [String] The parsed digitable line
137
+ def parse_digitable_line(digitable_line)
138
+ result = ''
139
+ DIGITABLE_LINE_TO_BOLETO_CONVERT_POSITIONS.each do |position|
140
+ result += digitable_line[position[:start]...position[:end]]
141
+ end
142
+ result
143
+ end
144
+
145
+ # Calculate mod11 for a given value string.
146
+ #
147
+ # @param value [String] The value string to calculate mod11 for
148
+ # @return [Integer] The mod11 check digit
149
+ def get_mod11(value)
150
+ weight = MOD11_WEIGHTS[:initial]
151
+ sum = 0
152
+ value_reversed = value.reverse
153
+
154
+ value_reversed.each_char do |char|
155
+ value_value = char.to_i
156
+ multiplier = value_value * weight
157
+
158
+ if weight < MOD11_WEIGHTS[:end]
159
+ weight += MOD11_WEIGHTS[:increment]
160
+ else
161
+ weight = MOD11_WEIGHTS[:initial]
162
+ end
163
+
164
+ sum += multiplier
165
+ end
166
+
167
+ mod11 = sum % 11
168
+ if mod11 != 0 && mod11 != 1
169
+ 11 - mod11
170
+ else
171
+ 1
172
+ end
173
+ end
174
+ end
175
+ end
176
+ end
@@ -0,0 +1,330 @@
1
+ require 'json'
2
+ require 'net/http'
3
+ require 'uri'
4
+
5
+ module BrazilianUtils
6
+ # Custom exceptions for CEP operations
7
+ class InvalidCEP < StandardError; end
8
+ class CEPNotFound < StandardError; end
9
+
10
+ # Brazilian states enum
11
+ module UF
12
+ STATES = {
13
+ 'AC' => 'Acre',
14
+ 'AL' => 'Alagoas',
15
+ 'AP' => 'Amapá',
16
+ 'AM' => 'Amazonas',
17
+ 'BA' => 'Bahia',
18
+ 'CE' => 'Ceará',
19
+ 'DF' => 'Distrito Federal',
20
+ 'ES' => 'Espírito Santo',
21
+ 'GO' => 'Goiás',
22
+ 'MA' => 'Maranhão',
23
+ 'MT' => 'Mato Grosso',
24
+ 'MS' => 'Mato Grosso do Sul',
25
+ 'MG' => 'Minas Gerais',
26
+ 'PA' => 'Pará',
27
+ 'PB' => 'Paraíba',
28
+ 'PR' => 'Paraná',
29
+ 'PE' => 'Pernambuco',
30
+ 'PI' => 'Piauí',
31
+ 'RJ' => 'Rio de Janeiro',
32
+ 'RN' => 'Rio Grande do Norte',
33
+ 'RS' => 'Rio Grande do Sul',
34
+ 'RO' => 'Rondônia',
35
+ 'RR' => 'Roraima',
36
+ 'SC' => 'Santa Catarina',
37
+ 'SP' => 'São Paulo',
38
+ 'SE' => 'Sergipe',
39
+ 'TO' => 'Tocantins'
40
+ }.freeze
41
+
42
+ def self.valid?(uf)
43
+ STATES.key?(uf.to_s.upcase)
44
+ end
45
+
46
+ def self.name_from_code(code)
47
+ STATES[code.to_s.upcase]
48
+ end
49
+
50
+ def self.code_from_name(name)
51
+ STATES.key(name)
52
+ end
53
+ end
54
+
55
+ # Address structure
56
+ Address = Struct.new(
57
+ :cep,
58
+ :logradouro,
59
+ :complemento,
60
+ :bairro,
61
+ :localidade,
62
+ :uf,
63
+ :ibge,
64
+ :gia,
65
+ :ddd,
66
+ :siafi,
67
+ keyword_init: true
68
+ )
69
+
70
+ module CEPUtils
71
+ # FORMATTING
72
+ ############
73
+
74
+ # Removes specific symbols from a given CEP (Postal Code).
75
+ #
76
+ # This function takes a CEP (Postal Code) as input and removes all occurrences
77
+ # of the '.' and '-' characters from it.
78
+ #
79
+ # @param dirty [String] The input CEP (Postal Code) containing symbols to be removed.
80
+ # @return [String] A new string with the specified symbols removed.
81
+ #
82
+ # @example
83
+ # remove_symbols("123-45.678.9") #=> "123456789"
84
+ # remove_symbols("abc.xyz") #=> "abcxyz"
85
+ def self.remove_symbols(dirty)
86
+ return '' unless dirty
87
+
88
+ dirty.to_s.delete('.-')
89
+ end
90
+
91
+ # Formats a Brazilian CEP (Postal Code) into a standard format.
92
+ #
93
+ # This function takes a CEP (Postal Code) as input and, if it is a valid
94
+ # 8-digit CEP, formats it into the standard "12345-678" format.
95
+ #
96
+ # @param cep [String] The input CEP (Postal Code) to be formatted.
97
+ # @return [String, nil] The formatted CEP in the "12345-678" format if it's valid,
98
+ # nil if it's not valid.
99
+ #
100
+ # @example
101
+ # format_cep("12345678") #=> "12345-678"
102
+ # format_cep("12345") #=> nil
103
+ def self.format_cep(cep)
104
+ return nil unless valid?(cep)
105
+
106
+ "#{cep[0..4]}-#{cep[5..7]}"
107
+ end
108
+
109
+ # OPERATIONS
110
+ ############
111
+
112
+ # Checks if a CEP (Postal Code) is valid.
113
+ #
114
+ # To be considered valid, the input must be a string containing exactly 8
115
+ # digits.
116
+ # This function does not verify if the CEP is a real postal code; it only
117
+ # validates the format of the string.
118
+ #
119
+ # @param cep [String] The string containing the CEP to be checked.
120
+ # @return [Boolean] true if the CEP is valid (8 digits), false otherwise.
121
+ #
122
+ # @example
123
+ # valid?("12345678") #=> true
124
+ # valid?("12345") #=> false
125
+ # valid?("abcdefgh") #=> false
126
+ #
127
+ # @see https://en.wikipedia.org/wiki/Código_de_Endereçamento_Postal
128
+ def self.valid?(cep)
129
+ return false unless cep.is_a?(String)
130
+
131
+ cep.length == 8 && cep.match?(/^\d{8}$/)
132
+ end
133
+
134
+ # Generates a random 8-digit CEP (Postal Code) number as a string.
135
+ #
136
+ # @return [String] A randomly generated 8-digit number.
137
+ #
138
+ # @example
139
+ # generate() #=> "12345678"
140
+ def self.generate
141
+ 8.times.map { rand(10) }.join
142
+ end
143
+
144
+ # API OPERATIONS
145
+ ################
146
+
147
+ # Fetches address information from a given CEP (Postal Code) using the ViaCEP API.
148
+ #
149
+ # @param cep [String] The CEP (Postal Code) to be used in the search.
150
+ # @param raise_exceptions [Boolean] Whether to raise exceptions when the CEP
151
+ # is invalid or not found. Defaults to false.
152
+ #
153
+ # @raise [InvalidCEP] When the input CEP is invalid.
154
+ # @raise [CEPNotFound] When the input CEP is not found.
155
+ #
156
+ # @return [Address, nil] An Address object containing the address information
157
+ # if the CEP is found, nil otherwise.
158
+ #
159
+ # @example
160
+ # get_address_from_cep("01310100")
161
+ # #=> #<Address cep="01310-100", logradouro="Avenida Paulista", ...>
162
+ #
163
+ # get_address_from_cep("abcdefg") #=> nil
164
+ #
165
+ # get_address_from_cep("abcdefg", true)
166
+ # #=> InvalidCEP: CEP 'abcdefg' is invalid.
167
+ #
168
+ # get_address_from_cep("00000000", true)
169
+ # #=> CEPNotFound: 00000000
170
+ #
171
+ # @see https://viacep.com.br/
172
+ def self.get_address_from_cep(cep, raise_exceptions: false)
173
+ base_api_url = 'https://viacep.com.br/ws/%s/json/'
174
+
175
+ clean_cep = remove_symbols(cep)
176
+ cep_is_valid = valid?(clean_cep)
177
+
178
+ unless cep_is_valid
179
+ raise InvalidCEP, "CEP '#{cep}' is invalid." if raise_exceptions
180
+
181
+ return nil
182
+ end
183
+
184
+ begin
185
+ uri = URI.parse(format(base_api_url, clean_cep))
186
+ response = Net::HTTP.get_response(uri)
187
+
188
+ unless response.is_a?(Net::HTTPSuccess)
189
+ raise CEPNotFound, cep if raise_exceptions
190
+
191
+ return nil
192
+ end
193
+
194
+ data = JSON.parse(response.body)
195
+
196
+ if data['erro']
197
+ raise CEPNotFound, cep if raise_exceptions
198
+
199
+ return nil
200
+ end
201
+
202
+ Address.new(
203
+ cep: data['cep'],
204
+ logradouro: data['logradouro'],
205
+ complemento: data['complemento'],
206
+ bairro: data['bairro'],
207
+ localidade: data['localidade'],
208
+ uf: data['uf'],
209
+ ibge: data['ibge'],
210
+ gia: data['gia'],
211
+ ddd: data['ddd'],
212
+ siafi: data['siafi']
213
+ )
214
+ rescue StandardError => e
215
+ raise CEPNotFound, cep if raise_exceptions
216
+
217
+ nil
218
+ end
219
+ end
220
+
221
+ # Fetches CEP (Postal Code) options from a given address using the ViaCEP API.
222
+ #
223
+ # @param federal_unit [String] The two-letter abbreviation of the Brazilian state.
224
+ # @param city [String] The name of the city.
225
+ # @param street [String] The name (or substring) of the street.
226
+ # @param raise_exceptions [Boolean] Whether to raise exceptions when the address
227
+ # is invalid or not found. Defaults to false.
228
+ #
229
+ # @raise [ArgumentError] When the input UF is invalid.
230
+ # @raise [CEPNotFound] When the input address is not found.
231
+ #
232
+ # @return [Array<Address>, nil] An array of Address objects containing the address
233
+ # information if the address is found, nil otherwise.
234
+ #
235
+ # @example
236
+ # get_cep_information_from_address("SP", "São Paulo", "Avenida Paulista")
237
+ # #=> [#<Address cep="01310-100", logradouro="Avenida Paulista", ...>, ...]
238
+ #
239
+ # get_cep_information_from_address("A", "Example", "Rua Example") #=> nil
240
+ #
241
+ # get_cep_information_from_address("XX", "Example", "Example", true)
242
+ # #=> ArgumentError: Invalid UF: XX
243
+ #
244
+ # get_cep_information_from_address("SP", "Example", "Example", true)
245
+ # #=> CEPNotFound: SP - Example - Example
246
+ #
247
+ # @see https://viacep.com.br/
248
+ def self.get_cep_information_from_address(federal_unit, city, street, raise_exceptions: false)
249
+ base_api_url = 'https://viacep.com.br/ws/%s/%s/%s/json/'
250
+
251
+ # Validate UF
252
+ uf_code = federal_unit.to_s.upcase
253
+ uf_name = UF.name_from_code(uf_code)
254
+
255
+ unless uf_name || UF.code_from_name(federal_unit)
256
+ if raise_exceptions
257
+ raise ArgumentError, "Invalid UF: #{federal_unit}"
258
+ end
259
+
260
+ return nil
261
+ end
262
+
263
+ # Use state name if code was provided
264
+ uf_to_use = uf_name ? federal_unit : UF.code_from_name(federal_unit)
265
+
266
+ # Normalize and encode city and street (remove accents and encode spaces)
267
+ parsed_city = normalize_string(city)
268
+ parsed_street = normalize_string(street)
269
+
270
+ begin
271
+ uri = URI.parse(format(base_api_url, uf_to_use, parsed_city, parsed_street))
272
+ response = Net::HTTP.get_response(uri)
273
+
274
+ unless response.is_a?(Net::HTTPSuccess)
275
+ if raise_exceptions
276
+ raise CEPNotFound, "#{federal_unit} - #{city} - #{street}"
277
+ end
278
+
279
+ return nil
280
+ end
281
+
282
+ data = JSON.parse(response.body)
283
+
284
+ if data.empty?
285
+ if raise_exceptions
286
+ raise CEPNotFound, "#{federal_unit} - #{city} - #{street}"
287
+ end
288
+
289
+ return nil
290
+ end
291
+
292
+ data.map do |address_data|
293
+ Address.new(
294
+ cep: address_data['cep'],
295
+ logradouro: address_data['logradouro'],
296
+ complemento: address_data['complemento'],
297
+ bairro: address_data['bairro'],
298
+ localidade: address_data['localidade'],
299
+ uf: address_data['uf'],
300
+ ibge: address_data['ibge'],
301
+ gia: address_data['gia'],
302
+ ddd: address_data['ddd'],
303
+ siafi: address_data['siafi']
304
+ )
305
+ end
306
+ rescue JSON::ParserError, StandardError => e
307
+ if raise_exceptions
308
+ raise CEPNotFound, "#{federal_unit} - #{city} - #{street}"
309
+ end
310
+
311
+ nil
312
+ end
313
+ end
314
+
315
+ private
316
+
317
+ # Normalizes a string by removing accents and encoding spaces for URL
318
+ def self.normalize_string(str)
319
+ require 'unicode_normalize/normalize'
320
+
321
+ str.to_s
322
+ .unicode_normalize(:nfd)
323
+ .encode('ASCII', undef: :replace, replace: '')
324
+ .gsub(' ', '%20')
325
+ rescue StandardError
326
+ # Fallback if unicode_normalize is not available
327
+ str.to_s.gsub(' ', '%20')
328
+ end
329
+ end
330
+ end
@@ -0,0 +1,88 @@
1
+ module BrazilianUtils
2
+ module CNHUtils
3
+ # Validates the registration number for the Brazilian CNH (Carteira Nacional de Habilitação)
4
+ # that was created in 2022.
5
+ #
6
+ # Previous versions of the CNH are not supported in this version.
7
+ # This function checks if the given CNH is valid based on the format and allowed characters,
8
+ # verifying the verification digits.
9
+ #
10
+ # @param cnh [String] CNH string (symbols will be ignored).
11
+ # @return [Boolean] true if CNH has a valid format, false otherwise.
12
+ #
13
+ # @example
14
+ # valid?("12345678901") #=> false
15
+ # valid?("A2C45678901") #=> false
16
+ # valid?("98765432100") #=> true
17
+ # valid?("987654321-00") #=> true
18
+ def self.valid?(cnh)
19
+ return false unless cnh
20
+
21
+ # Clean the input and check for numbers only
22
+ clean_cnh = cnh.to_s.gsub(/\D/, '')
23
+
24
+ return false if clean_cnh.empty?
25
+ return false if clean_cnh.length != 11
26
+
27
+ # Reject sequences as "00000000000", "11111111111", etc.
28
+ return false if clean_cnh == clean_cnh[0] * 11
29
+
30
+ # Cast digits to array of integers
31
+ digits = clean_cnh.chars.map(&:to_i)
32
+ first_verificator = digits[9]
33
+ second_verificator = digits[10]
34
+
35
+ # Check the 10th digit
36
+ return false unless check_first_verificator(digits, first_verificator)
37
+
38
+ # Check the 11th digit
39
+ check_second_verificator(digits, second_verificator, first_verificator)
40
+ end
41
+
42
+ # Generates the first verification digit and uses it to verify the 10th digit of the CNH
43
+ #
44
+ # @param digits [Array<Integer>] Array of CNH digits
45
+ # @param first_verificator [Integer] The first verification digit (10th digit)
46
+ # @return [Boolean] true if the first verificator is valid
47
+ #
48
+ # @private
49
+ def self.check_first_verificator(digits, first_verificator)
50
+ sum = 0
51
+ 9.times do |i|
52
+ sum += digits[i] * (9 - i)
53
+ end
54
+
55
+ sum = sum % 11
56
+ result = sum > 9 ? 0 : sum
57
+
58
+ result == first_verificator
59
+ end
60
+
61
+ # Generates the second verification digit and uses it to verify the 11th digit of the CNH
62
+ #
63
+ # @param digits [Array<Integer>] Array of CNH digits
64
+ # @param second_verificator [Integer] The second verification digit (11th digit)
65
+ # @param first_verificator [Integer] The first verification digit (10th digit)
66
+ # @return [Boolean] true if the second verificator is valid
67
+ #
68
+ # @private
69
+ def self.check_second_verificator(digits, second_verificator, first_verificator)
70
+ sum = 0
71
+ 9.times do |i|
72
+ sum += digits[i] * (i + 1)
73
+ end
74
+
75
+ result = sum % 11
76
+
77
+ if first_verificator > 9
78
+ result = (result - 2).negative? ? result + 9 : result - 2
79
+ end
80
+
81
+ result = 0 if result > 9
82
+
83
+ result == second_verificator
84
+ end
85
+
86
+ private_class_method :check_first_verificator, :check_second_verificator
87
+ end
88
+ end