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
@@ -1,235 +1,238 @@
1
- module BrazilianUtils
2
- # Utilities for consulting and validating the official *Natureza Jurídica* (Legal Nature)
3
- # codes defined by the Receita Federal do Brasil (RFB).
4
- #
5
- # The codes and descriptions in this module are sourced from the official
6
- # **Tabela de Natureza Jurídica** (RFB), as provided in the document used
7
- # by the Cadastro Nacional (e.g., FCN).
8
- #
9
- # This module offers simple lookups and validation helpers based on the official table.
10
- # It does not infer the current legal/registration status of any entity.
11
- #
12
- # Source: https://www.gov.br/empresas-e-negocios/pt-br/drei/links-e-downloads/arquivos/TABELADENATUREZAJURDICA.pdf
13
- module LegalNatureUtils
14
- # Official Legal Nature codes from Receita Federal do Brasil
15
- # Format: 4-digit code => Description in Portuguese
16
- LEGAL_NATURE = {
17
- # 1. ADMINISTRAÇÃO PÚBLICA
18
- '1015' => 'Órgão Público do Poder Executivo Federal',
19
- '1023' => 'Órgão Público do Poder Executivo Estadual ou do Distrito Federal',
20
- '1031' => 'Órgão Público do Poder Executivo Municipal',
21
- '1040' => 'Órgão Público do Poder Legislativo Federal',
22
- '1058' => 'Órgão Público do Poder Legislativo Estadual ou do Distrito Federal',
23
- '1066' => 'Órgão Público do Poder Legislativo Municipal',
24
- '1074' => 'Órgão Público do Poder Judiciário Federal',
25
- '1082' => 'Órgão Público do Poder Judiciário Estadual',
26
- '1104' => 'Autarquia Federal',
27
- '1112' => 'Autarquia Estadual ou do Distrito Federal',
28
- '1120' => 'Autarquia Municipal',
29
- '1139' => 'Fundação Federal',
30
- '1147' => 'Fundação Estadual ou do Distrito Federal',
31
- '1155' => 'Fundação Municipal',
32
- '1163' => 'Órgão Público Autônomo da União',
33
- '1171' => 'Órgão Público Autônomo Estadual ou do Distrito Federal',
34
- '1180' => 'Órgão Público Autônomo Municipal',
35
-
36
- # 2. ENTIDADES EMPRESARIAIS
37
- '2011' => 'Empresa Pública',
38
- '2038' => 'Sociedade de Economia Mista',
39
- '2046' => 'Sociedade Anônima Aberta',
40
- '2054' => 'Sociedade Anônima Fechada',
41
- '2062' => 'Sociedade Empresária Limitada',
42
- '2076' => 'Sociedade Empresária em Nome Coletivo',
43
- '2089' => 'Sociedade Empresária em Comandita Simples',
44
- '2097' => 'Sociedade Empresária em Comandita por Ações',
45
- '2100' => 'Sociedade Mercantil de Capital e Indústria (extinta pelo NCC/2002)',
46
- '2127' => 'Sociedade Empresária em Conta de Participação',
47
- '2135' => 'Empresário (Individual)',
48
- '2143' => 'Cooperativa',
49
- '2151' => 'Consórcio de Sociedades',
50
- '2160' => 'Grupo de Sociedades',
51
- '2178' => 'Estabelecimento, no Brasil, de Sociedade Estrangeira',
52
- '2194' => 'Estabelecimento, no Brasil, de Empresa Binacional Argentino-Brasileira',
53
- '2208' => 'Entidade Binacional Itaipu',
54
- '2216' => 'Empresa Domiciliada no Exterior',
55
- '2224' => 'Clube/Fundo de Investimento',
56
- '2232' => 'Sociedade Simples Pura',
57
- '2240' => 'Sociedade Simples Limitada',
58
- '2259' => 'Sociedade em Nome Coletivo',
59
- '2267' => 'Sociedade em Comandita Simples',
60
- '2275' => 'Sociedade Simples em Conta de Participação',
61
- '2305' => 'Empresa Individual de Responsabilidade Limitada',
62
-
63
- # 3. ENTIDADES SEM FINS LUCRATIVOS
64
- '3034' => 'Serviço Notarial e Registral (Cartório)',
65
- '3042' => 'Organização Social',
66
- '3050' => 'Organização da Sociedade Civil de Interesse Público (Oscip)',
67
- '3069' => 'Outras Formas de Fundações Mantidas com Recursos Privados',
68
- '3077' => 'Serviço Social Autônomo',
69
- '3085' => 'Condomínio Edilícios',
70
- '3093' => 'Unidade Executora (Programa Dinheiro Direto na Escola)',
71
- '3107' => 'Comissão de Conciliação Prévia',
72
- '3115' => 'Entidade de Mediação e Arbitragem',
73
- '3123' => 'Partido Político',
74
- '3131' => 'Entidade Sindical',
75
- '3204' => 'Estabelecimento, no Brasil, de Fundação ou Associação Estrangeiras',
76
- '3212' => 'Fundação ou Associação Domiciliada no Exterior',
77
- '3999' => 'Outras Formas de Associação',
78
-
79
- # 4. PESSOAS FÍSICAS
80
- '4014' => 'Empresa Individual Imobiliária',
81
- '4022' => 'Segurado Especial',
82
- '4081' => 'Contribuinte individual',
83
-
84
- # 5. ORGANIZAÇÕES INTERNACIONAIS E OUTRAS INSTITUIÇÕES EXTRATERRITORIAIS
85
- '5002' => 'Organização Internacional e Outras Instituições Extraterritoriais'
86
- }.freeze
87
-
88
- # Normalizes a legal nature code to 4-digit format.
89
- # Accepts formats like "2062", "206-2", or any string with exactly 4 digits.
90
- #
91
- # @param code [String] The code to normalize
92
- # @return [String, nil] The normalized 4-digit code, or nil if invalid
93
- #
94
- # @private
95
- def self.normalize(code)
96
- return nil unless code.is_a?(String)
97
-
98
- # Extract only digits from the input
99
- digits = code.strip.gsub(/\D/, '')
100
-
101
- # Return the digits only if we have exactly 4
102
- digits.length == 4 ? digits : nil
103
- end
104
-
105
- private_class_method :normalize
106
-
107
- # Checks if a string corresponds to a valid *Natureza Jurídica* code.
108
- #
109
- # Validation is based solely on the presence of the code in the official RFB table.
110
- # It does not verify the current legal status or registration of the entity.
111
- #
112
- # @param code [String] The code to be validated. Accepts either "NNNN" or "NNN-N" format.
113
- #
114
- # @return [Boolean] Returns true if the normalized code exists in the official table,
115
- # false otherwise.
116
- #
117
- # @example Valid codes
118
- # is_valid("2062") #=> true (Sociedade Empresária Limitada)
119
- # is_valid("206-2") #=> true (same, with hyphen)
120
- # is_valid("1015") #=> true (Órgão Público Federal)
121
- # is_valid("101-5") #=> true (same, with hyphen)
122
- #
123
- # @example Invalid codes
124
- # is_valid("9999") #=> false (not in official table)
125
- # is_valid("0000") #=> false (not in official table)
126
- # is_valid("123") #=> false (wrong length)
127
- # is_valid("abcd") #=> false (not digits)
128
- # is_valid(nil) #=> false (not a string)
129
- def self.is_valid(code)
130
- normalized = normalize(code)
131
- return false unless normalized
132
-
133
- LEGAL_NATURE.key?(normalized)
134
- end
135
-
136
- # Alias for is_valid to provide Ruby-style naming
137
- class << self
138
- alias valid? is_valid
139
- end
140
-
141
- # Retrieves the description of a *Natureza Jurídica* code.
142
- #
143
- # @param code [String] The code to look up. Accepts either "NNNN" or "NNN-N" format.
144
- #
145
- # @return [String, nil] The full description if the code is valid, otherwise nil.
146
- #
147
- # @example Valid lookups
148
- # get_description("2062")
149
- # #=> "Sociedade Empresária Limitada"
150
- #
151
- # get_description("101-5")
152
- # #=> "Órgão Público do Poder Executivo Federal"
153
- #
154
- # get_description("2305")
155
- # #=> "Empresa Individual de Responsabilidade Limitada"
156
- #
157
- # @example Invalid lookups
158
- # get_description("0000")
159
- # #=> nil
160
- #
161
- # get_description("invalid")
162
- # #=> nil
163
- def self.get_description(code)
164
- normalized = normalize(code)
165
- return nil unless normalized
166
-
167
- LEGAL_NATURE[normalized]
168
- end
169
-
170
- # Returns a copy of the full *Natureza Jurídica* table.
171
- #
172
- # @return [Hash<String, String>] Mapping from 4-digit codes to descriptions
173
- #
174
- # @example
175
- # all_codes = list_all
176
- # all_codes["2062"]
177
- # #=> "Sociedade Empresária Limitada"
178
- #
179
- # all_codes.size
180
- # #=> 60 (total number of codes in the official table)
181
- def self.list_all
182
- LEGAL_NATURE.dup
183
- end
184
-
185
- # Returns all codes within a specific category.
186
- #
187
- # Categories:
188
- # - 1: Administração Pública (Public Administration)
189
- # - 2: Entidades Empresariais (Business Entities)
190
- # - 3: Entidades Sem Fins Lucrativos (Non-Profit Entities)
191
- # - 4: Pessoas Físicas (Individuals)
192
- # - 5: Organizações Internacionais (International Organizations)
193
- #
194
- # @param category [Integer, String] The category number (1-5)
195
- #
196
- # @return [Hash<String, String>] Codes and descriptions for the specified category
197
- #
198
- # @example
199
- # business_entities = list_by_category(2)
200
- # business_entities.keys
201
- # #=> ["2011", "2038", "2046", ...]
202
- #
203
- # non_profits = list_by_category(3)
204
- # non_profits["3123"]
205
- # #=> "Partido Político"
206
- def self.list_by_category(category)
207
- category_str = category.to_s
208
- return {} unless ['1', '2', '3', '4', '5'].include?(category_str)
209
-
210
- LEGAL_NATURE.select { |code, _| code.start_with?(category_str) }
211
- end
212
-
213
- # Returns the category number for a given code.
214
- #
215
- # @param code [String] The code to check. Accepts either "NNNN" or "NNN-N" format.
216
- #
217
- # @return [Integer, nil] The category number (1-5), or nil if invalid
218
- #
219
- # @example
220
- # get_category("2062")
221
- # #=> 2 (Entidades Empresariais)
222
- #
223
- # get_category("101-5")
224
- # #=> 1 (Administração Pública)
225
- #
226
- # get_category("9999")
227
- # #=> nil (invalid code)
228
- def self.get_category(code)
229
- normalized = normalize(code)
230
- return nil unless normalized && LEGAL_NATURE.key?(normalized)
231
-
232
- normalized[0].to_i
233
- end
234
- end
235
- end
1
+ require 'json'
2
+
3
+ module BrazilianUtils
4
+ # Utilities for consulting and validating the official *Natureza Jurídica* (Legal Nature)
5
+ # codes defined by IBGE/CONCLA and the Receita Federal do Brasil (RFB).
6
+ #
7
+ # The table backing this module is CONCLA's Natureza Jurídica 2021 table:
8
+ # 92 codes currently in force, plus a best-effort list of codes retired by
9
+ # past revisions (see `data/legal_nature.json`).
10
+ #
11
+ # This module offers simple lookups and validation helpers based on the
12
+ # official table. It does not infer the current legal/registration status
13
+ # of any entity.
14
+ #
15
+ # Source: https://concla.ibge.gov.br/images/concla/documentacao/CONCLA-TNJ2021-NotasExplicativas.pdf
16
+ module LegalNatureUtils
17
+ DATA_FILE = File.join(File.dirname(__FILE__), 'data', 'legal_nature.json')
18
+
19
+ # @private
20
+ def self.load_data
21
+ return @data if @data
22
+
23
+ raw = JSON.parse(File.read(DATA_FILE))
24
+ in_force = {}
25
+ raw['inForce'].each { |row| in_force[row['code']] = row }
26
+ retired = {}
27
+ raw['retired'].each { |row| retired[row['code']] = row }
28
+ @data = { in_force: in_force, retired: retired }
29
+ end
30
+
31
+ private_class_method :load_data
32
+
33
+ # Normalizes a legal nature code: strips hyphens, dots and whitespace,
34
+ # and requires exactly 4 digits.
35
+ #
36
+ # @param code [String, Integer] The code to normalize
37
+ # @return [String, nil] The normalized 4-digit code, or nil if invalid
38
+ #
39
+ # @private
40
+ def self.normalize(code)
41
+ return nil unless code.is_a?(String) || code.is_a?(Integer)
42
+
43
+ digits = code.to_s.gsub(/[-.\s]/, '')
44
+ digits.match?(/\A\d{4}\z/) ? digits : nil
45
+ end
46
+
47
+ private_class_method :normalize
48
+
49
+ # @private
50
+ def self.entry_to_hash(row)
51
+ entry = {
52
+ code: row['code'],
53
+ description: row['description'],
54
+ category: { code: row['category']['code'], description: row['category']['description'] },
55
+ legacy: row['legacy']
56
+ }
57
+ entry[:currentCode] = row['currentCode'] if row['legacy']
58
+ entry
59
+ end
60
+
61
+ private_class_method :entry_to_hash
62
+
63
+ # Checks if a string corresponds to a valid *Natureza Jurídica* code.
64
+ #
65
+ # Accepts both the 92 codes currently in force and the (best-effort)
66
+ # list of codes retired by a past revision.
67
+ #
68
+ # @param code [String] The code to be validated. Accepts "NNNN" or
69
+ # "NNN-N", with hyphens/dots/whitespace tolerated around the digits.
70
+ #
71
+ # @return [Boolean] Returns true if the normalized code exists in the
72
+ # official table (in force or retired), false otherwise.
73
+ #
74
+ # @example Valid codes
75
+ # is_valid("2062") #=> true (Sociedade Empresária Limitada)
76
+ # is_valid("206-2") #=> true (same, with hyphen)
77
+ #
78
+ # @example Invalid codes
79
+ # is_valid("9999") #=> false (not in official table)
80
+ # is_valid("abcd") #=> false (not digits)
81
+ # is_valid(nil) #=> false (not a string)
82
+ def self.is_valid(code)
83
+ normalized = normalize(code)
84
+ return false unless normalized
85
+
86
+ data = load_data
87
+ data[:in_force].key?(normalized) || data[:retired].key?(normalized)
88
+ end
89
+
90
+ class << self
91
+ alias valid? is_valid
92
+ end
93
+
94
+ # Retrieves the description of a *Natureza Jurídica* code.
95
+ #
96
+ # @param code [String] The code to look up. Accepts "NNNN" or "NNN-N".
97
+ #
98
+ # @return [String, nil] The full description if the code is valid, otherwise nil.
99
+ #
100
+ # @example
101
+ # get_description("2062") #=> "Sociedade Empresária Limitada"
102
+ # get_description("101-5") #=> "Órgão Público do Poder Executivo Federal"
103
+ def self.get_description(code)
104
+ normalized = normalize(code)
105
+ return nil unless normalized
106
+
107
+ data = load_data
108
+ row = data[:in_force][normalized] || data[:retired][normalized]
109
+ row && row['description']
110
+ end
111
+
112
+ # Looks a legal nature code up in the IBGE/CONCLA Natureza Jurídica 2021
113
+ # table.
114
+ #
115
+ # @param value [String, Integer] The code to look up ("NNNN" or "NNN-N").
116
+ # @return [Hash, nil] `{ code:, description:, category: { code:,
117
+ # description: }, legacy:, currentCode: (only when legacy) }`, or nil
118
+ # for an unknown code.
119
+ #
120
+ # @example
121
+ # get("2062") #=> { code: "2062", description: "Sociedade Empresária Limitada", category: { code: "2", description: "Entidades Empresariais" }, legacy: false }
122
+ # get("2208") #=> { code: "2208", description: "Entidade Binacional Itaipu", ..., legacy: true, currentCode: "2275" }
123
+ def self.get(value)
124
+ normalized = normalize(value)
125
+ return nil unless normalized
126
+
127
+ data = load_data
128
+ row = data[:in_force][normalized] || data[:retired][normalized]
129
+ row && entry_to_hash(row)
130
+ end
131
+
132
+ # Formats a legal nature code as `NNN-N`.
133
+ #
134
+ # The mask is applied as far as the digits go (fewer than 4 digits are
135
+ # returned unmasked); use {is_valid} to check the code first.
136
+ #
137
+ # @param value [String, Integer] The value to format.
138
+ # @param options [Hash] `:pad` left-pads the value with zeros to 4
139
+ # digits first.
140
+ # @return [String] The masked code, or as much of the mask as fits.
141
+ #
142
+ # @example
143
+ # format("2062") #=> "206-2"
144
+ # format("206-2") #=> "206-2"
145
+ # format("206") #=> "206"
146
+ def self.format(value, options = {})
147
+ digits = value.to_s.gsub(/\D/, '')
148
+ digits = digits.rjust(4, '0') if options[:pad] || options['pad']
149
+ return digits if digits.length < 4
150
+
151
+ "#{digits[0, 3]}-#{digits[3]}"
152
+ end
153
+
154
+ # Generates a random valid legal nature code (4 digits), drawn only
155
+ # among the 92 codes currently in force.
156
+ #
157
+ # @return [String] A randomly generated, valid legal nature code.
158
+ def self.generate
159
+ load_data[:in_force].keys.sample
160
+ end
161
+
162
+ # Returns a copy of the *Natureza Jurídica* table as a map from code to
163
+ # description.
164
+ #
165
+ # @param params [Hash] `:include_retired` also includes the retired
166
+ # codes (default false: only the 92 codes in force).
167
+ # @return [Hash<String, String>] Mapping from 4-digit codes to descriptions
168
+ #
169
+ # @example
170
+ # all_codes = list
171
+ # all_codes["2062"] #=> "Sociedade Empresária Limitada"
172
+ def self.list(params = {})
173
+ include_retired = params[:include_retired] || params['include_retired']
174
+ data = load_data
175
+ result = {}
176
+ data[:in_force].each { |code, row| result[code] = row['description'] }
177
+ data[:retired].each { |code, row| result[code] = row['description'] } if include_retired
178
+ result
179
+ end
180
+
181
+ class << self
182
+ alias list_all list
183
+ end
184
+
185
+ # Returns every legal nature of a CONCLA category (the first digit of
186
+ # the code), sorted by code.
187
+ #
188
+ # Categories: 1 Administração Pública, 2 Entidades Empresariais,
189
+ # 3 Entidades sem Fins Lucrativos, 4 Pessoas Físicas,
190
+ # 5 Organizações Internacionais e Outras Instituições Extraterritoriais.
191
+ #
192
+ # @param category [Integer, String] The category number (1-5)
193
+ # @param options [Hash] `:include_retired` also includes retired codes.
194
+ #
195
+ # @return [Array<Hash>] The legal natures of the category, in the same
196
+ # shape as {get}, sorted by code; an empty array for an unknown
197
+ # category.
198
+ def self.list_by_category(category, options = {})
199
+ category_str = category.to_s
200
+ return [] unless %w[1 2 3 4 5].include?(category_str)
201
+
202
+ include_retired = options[:include_retired] || options['include_retired']
203
+ data = load_data
204
+
205
+ rows = data[:in_force].values.select { |row| row['category']['code'] == category_str }
206
+ rows += data[:retired].values.select { |row| row['category']['code'] == category_str } if include_retired
207
+
208
+ rows.sort_by { |row| row['code'] }.map { |row| entry_to_hash(row) }
209
+ end
210
+
211
+ # Returns the category number for a given code.
212
+ #
213
+ # @param code [String] The code to check. Accepts "NNNN" or "NNN-N".
214
+ #
215
+ # @return [Integer, nil] The category number (1-5), or nil if invalid
216
+ #
217
+ # @example
218
+ # get_category("2062") #=> 2 (Entidades Empresariais)
219
+ # get_category("9999") #=> nil (invalid code)
220
+ def self.get_category(code)
221
+ entry = get(code)
222
+ entry && entry[:category][:code].to_i
223
+ end
224
+
225
+ # Removes legal nature formatting and keeps only digits, capped to 4 digits.
226
+ #
227
+ # @param value [String, Integer] A legal nature code, with or without formatting.
228
+ # @return [String] The parsed digits.
229
+ #
230
+ # @example
231
+ # parse("206-2") #=> "2062"
232
+ def self.parse(value)
233
+ return '' unless value.is_a?(String) || value.is_a?(Integer)
234
+
235
+ value.to_s.gsub(/\D/, '')[0, 4]
236
+ end
237
+ end
238
+ end
@@ -131,10 +131,12 @@ module BrazilianUtils
131
131
 
132
132
  # Generates a random legal process ID.
133
133
  #
134
- # @param year [Integer] The year for the legal process ID (default is current year).
134
+ # @param year [Integer, Hash] The year for the legal process ID (default
135
+ # is current year), or a single options Hash (as the contract's
136
+ # `GenerateProcessoJuridicoParams`) with `:year` and `:court`.
135
137
  # The year should not be in the past.
136
138
  # @param orgao [Integer] The judicial segment code (1-9) for the legal process ID
137
- # (default is random)
139
+ # (default is random). Ignored when `year` is a Hash.
138
140
  #
139
141
  # @return [String, nil] A randomly generated legal process ID (20 digits),
140
142
  # or nil if arguments are invalid
@@ -146,9 +148,19 @@ module BrazilianUtils
146
148
  # generate()
147
149
  # #=> "88031888120233030000" (uses current year and random orgao)
148
150
  #
151
+ # generate(year: 2023, court: 5)
152
+ # #=> "51659517020235080562" (contract-style options Hash)
153
+ #
149
154
  # generate(2022, 10)
150
155
  # #=> nil (year in the past, orgao out of range)
151
- def self.generate(year = Time.now.year, orgao = rand(1..9))
156
+ def self.generate(year = Time.now.year, orgao = nil)
157
+ if year.is_a?(Hash)
158
+ options = year
159
+ year = options[:year] || options['year'] || Time.now.year
160
+ orgao = options[:court] || options[:orgao] || options['court'] || options['orgao']
161
+ end
162
+ orgao ||= rand(1..9)
163
+
152
164
  return nil if year < Time.now.year
153
165
  return nil unless (1..9).include?(orgao)
154
166
 
@@ -238,5 +250,21 @@ module BrazilianUtils
238
250
  end
239
251
 
240
252
  private_class_method :load_legal_process_data
253
+
254
+ # Removes legal process formatting and keeps only digits, capped to 20
255
+ # digits.
256
+ #
257
+ # @param value [String, Integer] A legal process number, with or without
258
+ # the CNJ mask.
259
+ # @return [String] The parsed digits.
260
+ #
261
+ # @example
262
+ # parse("0002080-25.2012.5.15.0049") #=> "00020802520125150049"
263
+ # parse("00020802520125150049123") #=> "00020802520125150049"
264
+ def self.parse(value)
265
+ return '' unless value.is_a?(String) || value.is_a?(Integer)
266
+
267
+ value.to_s.gsub(/\D/, '')[0, 20]
268
+ end
241
269
  end
242
270
  end
@@ -34,19 +34,19 @@ module BrazilianUtils
34
34
  # #=> "ABC1C34"
35
35
  #
36
36
  # convert_to_mercosul("ABC4*67")
37
- # #=> nil
37
+ # #=> ""
38
38
  def self.convert_to_mercosul(license_plate)
39
- return nil unless license_plate.is_a?(String)
39
+ return '' unless license_plate.is_a?(String)
40
40
 
41
41
  clean = remove_symbols(license_plate).upcase
42
- return nil unless valid_old_format?(clean)
42
+ return '' unless valid_old_format?(clean)
43
43
 
44
44
  chars = clean.chars
45
-
45
+
46
46
  # Convert the 5th character (index 4) - the first digit after the letters
47
47
  # 0→A, 1→B, 2→C, etc.
48
48
  chars[4] = ('A'.ord + chars[4].to_i).chr
49
-
49
+
50
50
  chars.join
51
51
  end
52
52
 
@@ -279,5 +279,21 @@ module BrazilianUtils
279
279
  end
280
280
 
281
281
  private_class_method :valid_mercosul?
282
+
283
+ # Removes separators from a license plate, upper-cases it and caps it to
284
+ # 7 characters.
285
+ #
286
+ # @param value [String] A license plate string, in any case, with or
287
+ # without separators.
288
+ # @return [String] The parsed value.
289
+ #
290
+ # @example
291
+ # parse("abc-1234") #=> "ABC1234"
292
+ # parse("abc123456") #=> "ABC1234"
293
+ def self.parse(value)
294
+ return '' unless value.is_a?(String)
295
+
296
+ remove_symbols(value).upcase[0, 7]
297
+ end
282
298
  end
283
299
  end
@@ -0,0 +1,58 @@
1
+ require 'json'
2
+ require_relative 'text-utils'
3
+
4
+ module BrazilianUtils
5
+ # Utilities for looking up Brazilian municipalities by IBGE code, sourced
6
+ # from the IBGE Localidades API (5,571 municipalities).
7
+ module MunicipalityUtils
8
+ DATA_FILE = File.join(File.dirname(__FILE__), 'data', 'municipalities.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
+ # @private
18
+ def self.to_symbolized(row)
19
+ { code: row['code'], name: row['name'], stateCode: row['stateCode'] }
20
+ end
21
+
22
+ private_class_method :to_symbolized
23
+
24
+ # Looks a municipality up by its 7-digit IBGE code.
25
+ #
26
+ # @param code [String, Integer]
27
+ # @return [Hash, nil]
28
+ def self.get_by_code(code)
29
+ return nil if code.nil?
30
+
31
+ str = code.to_s.gsub(/\D/, '')
32
+ return nil unless str.length == 7
33
+
34
+ row = load_data.find { |r| r['code'] == str }
35
+ row && to_symbolized(row)
36
+ end
37
+
38
+ # Returns Brazilian municipalities, sorted by name (pt-BR collation).
39
+ #
40
+ # @param state_code [String, nil] Only that state's municipalities; when
41
+ # omitted, every municipality. An empty or unknown code returns an
42
+ # empty list (matching is case-sensitive).
43
+ # @return [Array<Hash>]
44
+ def self.list(state_code = nil)
45
+ rows = load_data
46
+
47
+ unless state_code.nil?
48
+ return [] unless state_code.is_a?(String) && !state_code.empty?
49
+
50
+ rows = rows.select { |r| r['stateCode'] == state_code }
51
+ end
52
+
53
+ rows
54
+ .sort_by { |r| TextUtils.remove_accents(r['name']).downcase }
55
+ .map { |r| to_symbolized(r) }
56
+ end
57
+ end
58
+ end