br-utils 0.1.1 → 0.2.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 (55) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +446 -22
  3. data/lib/brazilian-utils/area-code-utils.rb +60 -0
  4. data/lib/brazilian-utils/bank-account-utils.rb +82 -0
  5. data/lib/brazilian-utils/bank-utils.rb +61 -0
  6. data/lib/brazilian-utils/boleto-utils.rb +139 -0
  7. data/lib/brazilian-utils/caepf-utils.rb +91 -0
  8. data/lib/brazilian-utils/cbo-utils.rb +74 -0
  9. data/lib/brazilian-utils/cei-utils.rb +79 -0
  10. data/lib/brazilian-utils/cep-utils.rb +25 -1
  11. data/lib/brazilian-utils/certidao-utils.rb +171 -0
  12. data/lib/brazilian-utils/cfop-utils.rb +74 -0
  13. data/lib/brazilian-utils/cnae-utils.rb +104 -0
  14. data/lib/brazilian-utils/cnh-utils.rb +53 -0
  15. data/lib/brazilian-utils/cno-utils.rb +76 -0
  16. data/lib/brazilian-utils/cnpj-utils.rb +125 -12
  17. data/lib/brazilian-utils/cns-utils.rb +110 -0
  18. data/lib/brazilian-utils/cpf-utils.rb +13 -0
  19. data/lib/brazilian-utils/credit-card-utils.rb +47 -0
  20. data/lib/brazilian-utils/csosn-utils.rb +55 -0
  21. data/lib/brazilian-utils/cst-utils.rb +103 -0
  22. data/lib/brazilian-utils/currency-utils.rb +347 -228
  23. data/lib/brazilian-utils/data/area_codes.json +676 -0
  24. data/lib/brazilian-utils/data/banks.json +357 -0
  25. data/lib/brazilian-utils/data/cbo.json +1 -0
  26. data/lib/brazilian-utils/data/cfop.json +1 -0
  27. data/lib/brazilian-utils/data/cnae.json +1 -0
  28. data/lib/brazilian-utils/data/csosn.json +42 -0
  29. data/lib/brazilian-utils/data/cst.json +294 -0
  30. data/lib/brazilian-utils/data/legal_nature.json +900 -0
  31. data/lib/brazilian-utils/data/municipalities.json +1 -0
  32. data/lib/brazilian-utils/data/ncm.json +1 -0
  33. data/lib/brazilian-utils/data/states.json +218 -0
  34. data/lib/brazilian-utils/date-utils.rb +504 -256
  35. data/lib/brazilian-utils/iban-utils.rb +120 -0
  36. data/lib/brazilian-utils/ie-utils.rb +84 -0
  37. data/lib/brazilian-utils/legal-nature-utils.rb +238 -235
  38. data/lib/brazilian-utils/legal-process-utils.rb +31 -3
  39. data/lib/brazilian-utils/license-plate-utils.rb +21 -5
  40. data/lib/brazilian-utils/municipality-utils.rb +58 -0
  41. data/lib/brazilian-utils/ncm-utils.rb +91 -0
  42. data/lib/brazilian-utils/nfe-key-utils.rb +158 -0
  43. data/lib/brazilian-utils/number-utils.rb +123 -0
  44. data/lib/brazilian-utils/passport-utils.rb +65 -0
  45. data/lib/brazilian-utils/phone-utils.rb +491 -280
  46. data/lib/brazilian-utils/pis-utils.rb +16 -1
  47. data/lib/brazilian-utils/pix-key-utils.rb +70 -0
  48. data/lib/brazilian-utils/pix-payload-utils.rb +253 -0
  49. data/lib/brazilian-utils/registro-profissional-utils.rb +112 -0
  50. data/lib/brazilian-utils/renavam-utils.rb +14 -0
  51. data/lib/brazilian-utils/state-utils.rb +105 -0
  52. data/lib/brazilian-utils/text-utils.rb +105 -0
  53. data/lib/brazilian-utils/vin-utils.rb +46 -0
  54. data/lib/brazilian-utils/voter-id-utils.rb +36 -0
  55. metadata +39 -1
@@ -0,0 +1,171 @@
1
+ module BrazilianUtils
2
+ # Utilities for the matrícula of a certidão de registro civil (art. 473
3
+ # of the Código Nacional de Normas da Corregedoria Nacional de Justiça):
4
+ # a 32-digit number grouped as 6-2-2-4-1-5-3-7-2 (registry CNS, acervo,
5
+ # serviço, ano, tipo do livro, livro, folha, termo, 2 check digits).
6
+ module CertidaoUtils
7
+ # Maps the single-digit "tipo do livro" code to its name.
8
+ BOOK_TYPES = {
9
+ '1' => 'birth',
10
+ '2' => 'marriage',
11
+ '3' => 'religiousMarriage',
12
+ '4' => 'death',
13
+ '5' => 'stillbirth',
14
+ '6' => 'banns',
15
+ '7' => 'other',
16
+ '8' => 'emancipation',
17
+ '9' => 'interdiction'
18
+ }.freeze
19
+
20
+ FIELD_SIZES = [6, 2, 2, 4, 1, 5, 3, 7, 2].freeze
21
+ FIELD_KEYS = %i[registryCns acervo service year type book page term checkDigits].freeze
22
+
23
+ # @private
24
+ def self.apply_mask(digits, group_sizes, separators)
25
+ chunks = []
26
+ idx = 0
27
+ group_sizes.each do |size|
28
+ break if idx >= digits.length
29
+
30
+ chunks << digits[idx, size]
31
+ idx += size
32
+ end
33
+ chunks.each_with_index.map { |c, i| i.zero? ? c : "#{separators[i - 1]}#{c}" }.join
34
+ end
35
+
36
+ private_class_method :apply_mask
37
+
38
+ # Formats a matrícula into the printed groups (6-2-2-4-1-5-3-7-2),
39
+ # separated by spaces, applied as far as the digits go.
40
+ #
41
+ # @param value [String, Integer]
42
+ # @param options [Hash] `:pad` left-pads with zeros to 32 digits first.
43
+ # @return [String]
44
+ def self.format(value, options = {})
45
+ digits = value.to_s.gsub(/\D/, '')
46
+ digits = digits.rjust(32, '0') if options[:pad] || options['pad']
47
+ return '' if digits.empty?
48
+
49
+ apply_mask(digits, FIELD_SIZES, [' '] * 8)
50
+ end
51
+
52
+ # Removes the formatting of a certidão matrícula and keeps only
53
+ # digits, capped to 32 digits.
54
+ #
55
+ # @param value [String, Integer]
56
+ # @return [String]
57
+ def self.parse(value)
58
+ return '' unless value.is_a?(String) || value.is_a?(Integer)
59
+
60
+ value.to_s.gsub(/\D/, '')[0, 32]
61
+ end
62
+
63
+ # @private
64
+ #
65
+ # Computes a single modulus-11 check digit over `base`: the weight
66
+ # starts at 2 for the rightmost character and increases by 1 moving
67
+ # left (no cap, no wraparound).
68
+ def self.check_digit(base)
69
+ weights = (2...(2 + base.length)).to_a.reverse
70
+ sum = base.chars.each_with_index.sum { |d, i| d.to_i * weights[i] }
71
+ remainder = sum % 11
72
+ remainder < 2 ? 0 : 11 - remainder
73
+ end
74
+
75
+ private_class_method :check_digit
76
+
77
+ # @private
78
+ #
79
+ # Only digits and the separators {space, '.', '-', '/'} are accepted
80
+ # anywhere in the string; any other character makes the whole value
81
+ # invalid. Returns the digits-only string, or nil when the input isn't
82
+ # a String/Integer or contains a disallowed character.
83
+ def self.clean_for_validation(value)
84
+ return nil unless value.is_a?(String) || value.is_a?(Integer)
85
+
86
+ raw = value.to_s
87
+ return nil unless raw.match?(%r{\A[\d\s.\-/]*\z})
88
+
89
+ raw.gsub(%r{[\s.\-/]}, '')
90
+ end
91
+
92
+ private_class_method :clean_for_validation
93
+
94
+ # @private
95
+ def self.extract_fields(digits)
96
+ fields = {}
97
+ idx = 0
98
+ FIELD_KEYS.each_with_index do |key, i|
99
+ size = FIELD_SIZES[i]
100
+ fields[key] = digits[idx, size]
101
+ idx += size
102
+ end
103
+ fields
104
+ end
105
+
106
+ private_class_method :extract_fields
107
+
108
+ # Checks whether a certidão matrícula is structurally valid and its 2
109
+ # modulus-11 check digits match.
110
+ #
111
+ # @param value [String, Integer] The matrícula, bare or separated by
112
+ # any mix of space, `.`, `-` or `/`. Any other character (a letter,
113
+ # for instance) anywhere in the value makes it invalid.
114
+ # @param options [Hash] `:accept` an optional Array narrowing which
115
+ # book-type codes (as the raw digit string, e.g. `"1"`) or names
116
+ # (e.g. `"birth"`) are accepted; defaults to accepting all of 1-9.
117
+ # @return [Boolean]
118
+ def self.is_valid(value, options = {})
119
+ digits = clean_for_validation(value)
120
+ return false unless digits
121
+ return false unless digits.match?(/\A\d{32}\z/)
122
+
123
+ raw = extract_fields(digits)
124
+ return false unless raw[:service] == '55'
125
+ return false unless BOOK_TYPES.key?(raw[:type])
126
+
127
+ accept = options[:accept] || options['accept']
128
+ if accept
129
+ accepted = Array(accept).map(&:to_s)
130
+ return false unless accepted.include?(raw[:type]) || accepted.include?(BOOK_TYPES[raw[:type]])
131
+ end
132
+
133
+ base30 = digits[0, 30]
134
+ dv1 = check_digit(base30)
135
+ dv2 = check_digit(base30 + dv1.to_s)
136
+
137
+ raw[:checkDigits] == "#{dv1}#{dv2}"
138
+ end
139
+
140
+ class << self
141
+ alias valid? is_valid
142
+ end
143
+
144
+ # Parses a certidão matrícula into its fields.
145
+ #
146
+ # @param value [String, Integer] Same accepted forms as {is_valid}.
147
+ # @param options [Hash] Same as {is_valid}.
148
+ # @return [Hash, nil] `nil` whenever {is_valid} would return false;
149
+ # otherwise a Hash with `:registryCns`, `:acervo`, `:service`,
150
+ # `:year` (Integer), `:type` (the book-type name), `:book`, `:page`,
151
+ # `:term` and `:checkDigits`.
152
+ def self.get_info(value, options = {})
153
+ return nil unless is_valid(value, options)
154
+
155
+ digits = clean_for_validation(value)
156
+ raw = extract_fields(digits)
157
+
158
+ {
159
+ registryCns: raw[:registryCns],
160
+ acervo: raw[:acervo],
161
+ service: raw[:service],
162
+ year: raw[:year].to_i,
163
+ type: BOOK_TYPES[raw[:type]],
164
+ book: raw[:book],
165
+ page: raw[:page],
166
+ term: raw[:term],
167
+ checkDigits: raw[:checkDigits]
168
+ }
169
+ end
170
+ end
171
+ end
@@ -0,0 +1,74 @@
1
+ require 'json'
2
+
3
+ module BrazilianUtils
4
+ # Utilities for the CFOP (Código Fiscal de Operações e Prestações) table,
5
+ # the consolidated Anexo II of Convênio SINIEF s/nº 1970 in force. Only
6
+ # operable codes are included (group/subgroup headings are excluded).
7
+ module CFOPUtils
8
+ DATA_FILE = File.join(File.dirname(__FILE__), 'data', 'cfop.json')
9
+
10
+ # @private
11
+ def self.load_data
12
+ @data ||= JSON.parse(File.read(DATA_FILE))
13
+ end
14
+
15
+ private_class_method :load_data
16
+
17
+ # Normalizes a CFOP value to its bare 4 digits, honoring the `N.NNN`
18
+ # single-separator mask; returns nil for anything else (nothing is
19
+ # padded, since no CFOP starts with a zero).
20
+ #
21
+ # @private
22
+ def self.normalize(value)
23
+ return nil if value.nil?
24
+ return nil unless value.is_a?(String) || value.is_a?(Integer)
25
+
26
+ raw = value.to_s.strip
27
+ return raw if raw.match?(/\A\d{4}\z/)
28
+
29
+ m = raw.match(%r{\A(\d)[ .\-/](\d{3})\z})
30
+ m && "#{m[1]}#{m[2]}"
31
+ end
32
+
33
+ private_class_method :normalize
34
+
35
+ # Checks whether a CFOP code exists in the official table (group and
36
+ # subgroup headings are not operable codes and are rejected).
37
+ #
38
+ # @param value [String, Integer]
39
+ # @return [Boolean]
40
+ def self.is_valid(value)
41
+ code = normalize(value)
42
+ return false unless code
43
+
44
+ load_data.key?(code)
45
+ end
46
+
47
+ class << self
48
+ alias valid? is_valid
49
+ end
50
+
51
+ # Looks a CFOP code up and returns its code and description.
52
+ #
53
+ # @param value [String, Integer]
54
+ # @return [Hash, nil]
55
+ def self.get(value)
56
+ code = normalize(value)
57
+ return nil unless code
58
+
59
+ description = load_data[code]
60
+ description && { code: code, description: description }
61
+ end
62
+
63
+ # Removes CFOP formatting and keeps only digits, capped to 4 digits
64
+ # (nothing is padded).
65
+ #
66
+ # @param value [String, Integer]
67
+ # @return [String]
68
+ def self.parse(value)
69
+ return '' unless value.is_a?(String) || value.is_a?(Integer)
70
+
71
+ value.to_s.gsub(/\D/, '')[0, 4]
72
+ end
73
+ end
74
+ end
@@ -0,0 +1,104 @@
1
+ require 'json'
2
+
3
+ module BrazilianUtils
4
+ # Utilities for the CNAE-Subclasses 2.3 table (the current subclass
5
+ # revision of CNAE 2.0), sourced from the IBGE CONCLA API.
6
+ module CNAEUtils
7
+ DATA_FILE = File.join(File.dirname(__FILE__), 'data', 'cnae.json')
8
+
9
+ # @private
10
+ def self.load_data
11
+ @data ||= JSON.parse(File.read(DATA_FILE))
12
+ end
13
+
14
+ private_class_method :load_data
15
+
16
+ # @private
17
+ def self.apply_mask(digits, group_sizes, separators)
18
+ chunks = []
19
+ idx = 0
20
+ group_sizes.each do |size|
21
+ break if idx >= digits.length
22
+
23
+ chunks << digits[idx, size]
24
+ idx += size
25
+ end
26
+ chunks.each_with_index.map { |c, i| i.zero? ? c : "#{separators[i - 1]}#{c}" }.join
27
+ end
28
+
29
+ private_class_method :apply_mask
30
+
31
+ # Normalizes a CNAE value to its bare 7 digits: bare digits (string or
32
+ # integer) are left-padded with zeros to 7; the `NNNN-N/NN` mask is
33
+ # read as written. Any other string is rejected.
34
+ #
35
+ # @private
36
+ def self.normalize(value)
37
+ return nil if value.nil?
38
+ return value.to_s.rjust(7, '0') if value.is_a?(Integer)
39
+ return nil unless value.is_a?(String)
40
+
41
+ raw = value.strip
42
+ return raw.rjust(7, '0') if raw.match?(/\A\d+\z/)
43
+
44
+ m = raw.match(%r{\A(\d{4})-(\d)/(\d{2})\z})
45
+ m && "#{m[1]}#{m[2]}#{m[3]}"
46
+ end
47
+
48
+ private_class_method :normalize
49
+
50
+ # Formats a CNAE subclass code with the mask `NNNN-N/NN`; the mask is
51
+ # applied as far as the digits go (only the structure changes — use
52
+ # {is_valid} to check the code).
53
+ #
54
+ # @param value [String, Integer]
55
+ # @param options [Hash] `:pad` left-pads with zeros to 7 digits first.
56
+ # @return [String] An empty string when there is no digit at all.
57
+ def self.format(value, options = {})
58
+ digits = value.to_s.gsub(/\D/, '')
59
+ digits = digits.rjust(7, '0') if options[:pad] || options['pad']
60
+ return '' if digits.empty?
61
+
62
+ apply_mask(digits, [4, 1, 2], ['-', '/'])
63
+ end
64
+
65
+ # Checks whether a CNAE subclass code exists in the table.
66
+ #
67
+ # @param value [String, Integer]
68
+ # @return [Boolean]
69
+ def self.is_valid(value)
70
+ code = normalize(value)
71
+ return false unless code
72
+
73
+ load_data.key?(code)
74
+ end
75
+
76
+ class << self
77
+ alias valid? is_valid
78
+ end
79
+
80
+ # Looks a CNAE subclass code up and returns its (bare, 7-digit) code
81
+ # and official description.
82
+ #
83
+ # @param value [String, Integer]
84
+ # @return [Hash, nil]
85
+ def self.get(value)
86
+ code = normalize(value)
87
+ return nil unless code
88
+
89
+ description = load_data[code]
90
+ description && { code: code, description: description }
91
+ end
92
+
93
+ # Removes CNAE formatting and keeps only digits, capped to 7 digits
94
+ # (nothing is left-padded).
95
+ #
96
+ # @param value [String, Integer]
97
+ # @return [String]
98
+ def self.parse(value)
99
+ return '' unless value.is_a?(String) || value.is_a?(Integer)
100
+
101
+ value.to_s.gsub(/\D/, '')[0, 7]
102
+ end
103
+ end
104
+ end
@@ -84,5 +84,58 @@ module BrazilianUtils
84
84
  end
85
85
 
86
86
  private_class_method :calculate_first_rest, :check_second_verificator
87
+
88
+ # Formats a CNH number as `000000000-00` (9 digits, hyphen, 2 check
89
+ # digits). The mask is applied as far as the digits go.
90
+ #
91
+ # @param value [String, Integer] The value to format.
92
+ # @param options [Hash] `:pad` left-pads the value with zeros to 11
93
+ # digits first.
94
+ # @return [String]
95
+ #
96
+ # @example
97
+ # format("00000000119") #=> "000000001-19"
98
+ def self.format(value, options = {})
99
+ digits = value.to_s.gsub(/\D/, '')
100
+ digits = digits.rjust(11, '0') if options[:pad] || options['pad']
101
+ return digits if digits.length <= 9
102
+
103
+ "#{digits[0, 9]}-#{digits[9..-1]}"
104
+ end
105
+
106
+ # Removes CNH formatting and keeps only digits, capped to 11 digits.
107
+ #
108
+ # @param value [String, Integer]
109
+ # @return [String]
110
+ #
111
+ # @example
112
+ # parse("000000001-19") #=> "00000000119"
113
+ def self.parse(value)
114
+ return '' unless value.is_a?(String) || value.is_a?(Integer)
115
+
116
+ value.to_s.gsub(/\D/, '')[0, 11]
117
+ end
118
+
119
+ # Generates a valid random CNH number (11 digits, unformatted).
120
+ #
121
+ # @return [String]
122
+ def self.generate
123
+ loop do
124
+ base9 = 9.times.map { rand(0..9) }
125
+ next if base9.uniq.length == 1
126
+
127
+ first_rest = calculate_first_rest(base9)
128
+ first_verificator = first_rest > 9 ? 0 : first_rest
129
+
130
+ sum2 = 0
131
+ 9.times { |i| sum2 += base9[i] * (i + 1) }
132
+ result = sum2 % 11
133
+ result = (result - 2).negative? ? result + 9 : result - 2 if first_rest > 9
134
+ second_verificator = result > 9 ? 0 : result
135
+
136
+ cnh = "#{base9.join}#{first_verificator}#{second_verificator}"
137
+ return cnh if valid?(cnh)
138
+ end
139
+ end
87
140
  end
88
141
  end
@@ -0,0 +1,76 @@
1
+ module BrazilianUtils
2
+ # Utilities for the CNO (Cadastro Nacional de Obras), which replaced the
3
+ # CEI for construction works and kept its numbering/check-digit rule.
4
+ module CNOUtils
5
+ WEIGHTS = [7, 4, 1, 8, 5, 2, 1, 6, 3, 7, 4].freeze
6
+
7
+ # @private
8
+ def self.apply_mask(digits, group_sizes, separators)
9
+ chunks = []
10
+ idx = 0
11
+ group_sizes.each do |size|
12
+ break if idx >= digits.length
13
+
14
+ chunks << digits[idx, size]
15
+ idx += size
16
+ end
17
+ chunks.each_with_index.map { |c, i| i.zero? ? c : "#{separators[i - 1]}#{c}" }.join
18
+ end
19
+
20
+ private_class_method :apply_mask
21
+
22
+ # @private
23
+ def self.check_digit(base)
24
+ sum = base.chars.each_with_index.sum { |d, i| d.to_i * WEIGHTS[i] }
25
+ last_two = sum % 100
26
+ combined = (last_two / 10) + (last_two % 10)
27
+ (10 - (combined % 10)) % 10
28
+ end
29
+
30
+ private_class_method :check_digit
31
+
32
+ # Validates a CNO: same rules as CEIUtils#is_valid.
33
+ #
34
+ # @param value [String, Integer]
35
+ # @return [Boolean]
36
+ def self.is_valid(value)
37
+ return false unless value.is_a?(String) || value.is_a?(Integer)
38
+
39
+ raw = value.to_s.strip
40
+ return false unless raw.match?(%r{\A[\d\s.\-/]+\z})
41
+
42
+ digits = raw.gsub(%r{[\s.\-/]}, '')
43
+ return false unless digits.match?(/\A\d{12}\z/)
44
+ return false if digits.chars.uniq.length == 1
45
+
46
+ digits[11].to_i == check_digit(digits[0, 11])
47
+ end
48
+
49
+ class << self
50
+ alias valid? is_valid
51
+ end
52
+
53
+ # Formats a CNO with the mask `00.000.00000/00` (the CEI numbering).
54
+ #
55
+ # @param value [String, Integer]
56
+ # @param options [Hash] `:pad` left-pads with zeros to 12 digits first.
57
+ # @return [String]
58
+ def self.format(value, options = {})
59
+ digits = value.to_s.gsub(/\D/, '')
60
+ digits = digits.rjust(12, '0') if options[:pad] || options['pad']
61
+ return '' if digits.empty?
62
+
63
+ apply_mask(digits, [2, 3, 5, 2], ['.', '.', '/'])
64
+ end
65
+
66
+ # Removes CNO formatting and keeps only digits, capped to 12 digits.
67
+ #
68
+ # @param value [String, Integer]
69
+ # @return [String]
70
+ def self.parse(value)
71
+ return '' unless value.is_a?(String) || value.is_a?(Integer)
72
+
73
+ value.to_s.gsub(/\D/, '')[0, 12]
74
+ end
75
+ end
76
+ end
@@ -120,34 +120,123 @@ module BrazilianUtils
120
120
  # validates the format of the string.
121
121
  #
122
122
  # @param cnpj [String] The CNPJ to be validated, a 14-digit string
123
+ # (v1, numeric) or a 14-character alphanumeric string (v2, when
124
+ # `version: 2` is given).
125
+ # @param version [Integer] `1` (default) validates the classic
126
+ # all-numeric CNPJ; `2` validates the alphanumeric CNPJ introduced by
127
+ # IN RFB 2.119.
123
128
  # @return [Boolean] true if the checksum digits match the base number, false otherwise.
124
129
  #
125
130
  # @example
126
- # valid?("03560714000142") #=> true
127
- # valid?("00111222000133") #=> false
128
- def self.valid?(cnpj)
129
- cnpj.is_a?(String) && validate(cnpj)
131
+ # valid?("03560714000142") #=> true
132
+ # valid?("00111222000133") #=> false
133
+ # valid?("12ABC34501DE35", version: 2)
134
+ def self.valid?(cnpj, version: 1)
135
+ return false unless cnpj.is_a?(String)
136
+
137
+ version.to_i == 2 ? valid_v2?(cnpj) : validate(cnpj)
130
138
  end
131
139
 
132
140
  # Generates a random valid CNPJ digit string.
133
141
  #
134
- # An optional branch number parameter can be given; it defaults to 1.
142
+ # An optional branch number parameter can be given; it defaults to 1
143
+ # (v1) or a random branch 1-9999 (v2, when not given).
135
144
  #
136
- # @param branch [Integer] An optional branch number to be included in the CNPJ.
145
+ # @param branch [Integer, nil] An optional branch number to be included
146
+ # in the CNPJ.
147
+ # @param version [Integer] `1` (default) generates the classic
148
+ # all-numeric CNPJ; `2` generates the alphanumeric CNPJ.
137
149
  # @return [String] A randomly generated valid CNPJ string.
138
150
  #
139
151
  # @example
140
- # generate() #=> "30180536000105"
141
- # generate(1234) #=> "01745284123455"
142
- def self.generate(branch: 1)
143
- branch = branch % 10_000
144
- branch = 1 if branch.zero?
145
- branch_str = branch.to_s.rjust(4, '0')
152
+ # generate() #=> "30180536000105"
153
+ # generate(branch: 1234) #=> "01745284123455"
154
+ # generate(version: 2) #=> "12ABC34501DE35"
155
+ def self.generate(branch: nil, version: 1)
156
+ return generate_v2(branch) if version.to_i == 2
157
+
158
+ branch_num = branch.nil? ? 1 : branch % 10_000
159
+ branch_num = 1 if branch_num.zero?
160
+ branch_str = branch_num.to_s.rjust(4, '0')
146
161
  base = format('%08d', rand(100_000_000)) + branch_str
147
162
 
148
163
  base + checksum(base)
149
164
  end
150
165
 
166
+ # V2 (ALPHANUMERIC, IN RFB 2.119)
167
+ #################################
168
+
169
+ # Characters usable in the alphanumeric CNPJ's base (positions 0-11).
170
+ V2_CHARSET = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ'.freeze
171
+
172
+ V2_WEIGHTS_DV1 = [5, 4, 3, 2, 9, 8, 7, 6, 5, 4, 3, 2].freeze
173
+ V2_WEIGHTS_DV2 = [6, 5, 4, 3, 2, 9, 8, 7, 6, 5, 4, 3, 2].freeze
174
+
175
+ # Maps a base-36 character (`0`-`9`, `A`-`Z`) to its numeric value for
176
+ # the v2 checksum: `'0'`..`'9'` -> 0..9, `'A'`..`'Z'` -> 17..42 (its
177
+ # ASCII code minus 48).
178
+ #
179
+ # @private
180
+ def self.v2_char_value(char)
181
+ char.ord - 48
182
+ end
183
+
184
+ private_class_method :v2_char_value
185
+
186
+ # Computes a single modulus-11 check digit over `chars` (a String of
187
+ # `0`-`9`/`A`-`Z` characters) using the given per-position weights.
188
+ #
189
+ # @private
190
+ def self.v2_hashdigit(chars, weights)
191
+ sum = chars.chars.each_with_index.sum { |c, i| v2_char_value(c) * weights[i] }
192
+ mod = sum % 11
193
+ mod < 2 ? 0 : 11 - mod
194
+ end
195
+
196
+ private_class_method :v2_hashdigit
197
+
198
+ # Validates a 14-character alphanumeric CNPJ (case-insensitive).
199
+ #
200
+ # @param cnpj [String]
201
+ # @return [Boolean]
202
+ #
203
+ # @private
204
+ def self.valid_v2?(cnpj)
205
+ upper = cnpj.to_s.upcase
206
+ return false unless upper.match?(/\A[0-9A-Z]{14}\z/)
207
+
208
+ dv1 = v2_hashdigit(upper[0, 12], V2_WEIGHTS_DV1)
209
+ return false unless dv1 == v2_char_value(upper[12])
210
+
211
+ dv2 = v2_hashdigit(upper[0, 13], V2_WEIGHTS_DV2)
212
+ dv2 == v2_char_value(upper[13])
213
+ end
214
+
215
+ private_class_method :valid_v2?
216
+
217
+ # Generates a random valid alphanumeric (v2) CNPJ.
218
+ #
219
+ # @param branch [Integer, nil] An optional branch number (1-9999); a
220
+ # random one is used when not given.
221
+ # @return [String]
222
+ #
223
+ # @private
224
+ def self.generate_v2(branch = nil)
225
+ branch_num = branch.nil? ? rand(1..9999) : branch % 10_000
226
+ branch_num = 1 if branch_num.zero?
227
+ branch_str = branch_num.to_s.rjust(4, '0')
228
+
229
+ base8 = Array.new(8) { V2_CHARSET[rand(V2_CHARSET.length)] }.join
230
+ base12 = base8 + branch_str
231
+
232
+ dv1 = v2_hashdigit(base12, V2_WEIGHTS_DV1)
233
+ dv2 = v2_hashdigit(base12 + dv1.to_s, V2_WEIGHTS_DV2)
234
+
235
+ "#{base12}#{dv1}#{dv2}"
236
+ end
237
+
238
+ private_class_method :generate_v2
239
+
151
240
  # PRIVATE METHODS
152
241
  #################
153
242
 
@@ -198,5 +287,29 @@ module BrazilianUtils
198
287
  end
199
288
 
200
289
  private_class_method :hashdigit, :checksum
290
+
291
+ # Removes CNPJ formatting and returns the normalized value, capped to 14
292
+ # characters.
293
+ #
294
+ # @param value [String, Integer] A CNPJ, with or without formatting.
295
+ # @param options [Hash] `:version` `1` (default) keeps digits only; `2`
296
+ # keeps letters and digits, upper-cased (the alphanumeric CNPJ format).
297
+ # @return [String] The parsed value.
298
+ #
299
+ # @example
300
+ # parse("46.843.485/0001-86") #=> "46843485000186"
301
+ def self.parse(value, options = {})
302
+ return '' unless value.is_a?(String) || value.is_a?(Integer)
303
+
304
+ version = (options[:version] || options['version'] || 1).to_i
305
+
306
+ cleaned = if version == 2
307
+ value.to_s.gsub(/[^a-zA-Z0-9]/, '').upcase
308
+ else
309
+ value.to_s.gsub(/\D/, '')
310
+ end
311
+
312
+ cleaned[0, 14]
313
+ end
201
314
  end
202
315
  end