br-utils 0.1.0 → 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.
- checksums.yaml +4 -4
- data/.github/workflows/ci.yml +27 -0
- data/README.md +446 -22
- data/lib/brazilian-utils/area-code-utils.rb +60 -0
- data/lib/brazilian-utils/bank-account-utils.rb +82 -0
- data/lib/brazilian-utils/bank-utils.rb +61 -0
- data/lib/brazilian-utils/boleto-utils.rb +139 -0
- data/lib/brazilian-utils/caepf-utils.rb +91 -0
- data/lib/brazilian-utils/cbo-utils.rb +74 -0
- data/lib/brazilian-utils/cei-utils.rb +79 -0
- data/lib/brazilian-utils/cep-utils.rb +34 -1
- data/lib/brazilian-utils/certidao-utils.rb +171 -0
- data/lib/brazilian-utils/cfop-utils.rb +74 -0
- data/lib/brazilian-utils/cnae-utils.rb +104 -0
- data/lib/brazilian-utils/cnh-utils.rb +68 -15
- data/lib/brazilian-utils/cno-utils.rb +76 -0
- data/lib/brazilian-utils/cnpj-utils.rb +126 -13
- data/lib/brazilian-utils/cns-utils.rb +110 -0
- data/lib/brazilian-utils/cpf-utils.rb +14 -1
- data/lib/brazilian-utils/credit-card-utils.rb +47 -0
- data/lib/brazilian-utils/csosn-utils.rb +55 -0
- data/lib/brazilian-utils/cst-utils.rb +103 -0
- data/lib/brazilian-utils/currency-utils.rb +347 -226
- data/lib/brazilian-utils/data/area_codes.json +676 -0
- data/lib/brazilian-utils/data/banks.json +357 -0
- data/lib/brazilian-utils/data/cbo.json +1 -0
- data/lib/brazilian-utils/data/cfop.json +1 -0
- data/lib/brazilian-utils/data/cnae.json +1 -0
- data/lib/brazilian-utils/data/csosn.json +42 -0
- data/lib/brazilian-utils/data/cst.json +294 -0
- data/lib/brazilian-utils/data/legal_nature.json +900 -0
- data/lib/brazilian-utils/data/municipalities.json +1 -0
- data/lib/brazilian-utils/data/ncm.json +1 -0
- data/lib/brazilian-utils/data/states.json +218 -0
- data/lib/brazilian-utils/date-utils.rb +504 -244
- data/lib/brazilian-utils/email-utils.rb +20 -9
- data/lib/brazilian-utils/iban-utils.rb +120 -0
- data/lib/brazilian-utils/ie-utils.rb +84 -0
- data/lib/brazilian-utils/legal-nature-utils.rb +238 -235
- data/lib/brazilian-utils/legal-process-utils.rb +35 -5
- data/lib/brazilian-utils/license-plate-utils.rb +24 -4
- data/lib/brazilian-utils/municipality-utils.rb +58 -0
- data/lib/brazilian-utils/ncm-utils.rb +91 -0
- data/lib/brazilian-utils/nfe-key-utils.rb +158 -0
- data/lib/brazilian-utils/number-utils.rb +123 -0
- data/lib/brazilian-utils/passport-utils.rb +65 -0
- data/lib/brazilian-utils/phone-utils.rb +491 -272
- data/lib/brazilian-utils/pis-utils.rb +16 -1
- data/lib/brazilian-utils/pix-key-utils.rb +70 -0
- data/lib/brazilian-utils/pix-payload-utils.rb +253 -0
- data/lib/brazilian-utils/registro-profissional-utils.rb +112 -0
- data/lib/brazilian-utils/renavam-utils.rb +14 -0
- data/lib/brazilian-utils/state-utils.rb +105 -0
- data/lib/brazilian-utils/text-utils.rb +105 -0
- data/lib/brazilian-utils/vin-utils.rb +46 -0
- data/lib/brazilian-utils/voter-id-utils.rb +70 -19
- metadata +54 -2
- data/.travis.yml +0 -5
|
@@ -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")
|
|
127
|
-
# valid?("00111222000133")
|
|
128
|
-
|
|
129
|
-
|
|
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
|
|
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()
|
|
141
|
-
# generate(1234)
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
branch
|
|
145
|
-
|
|
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
|
|
|
@@ -170,7 +259,7 @@ module BrazilianUtils
|
|
|
170
259
|
(position - 8).downto(2) { |w| weights << w }
|
|
171
260
|
9.downto(2) { |w| weights << w }
|
|
172
261
|
|
|
173
|
-
val = cnpj.chars.zip(weights).sum do |digit, weight|
|
|
262
|
+
val = cnpj.chars.first(position - 1).zip(weights).sum do |digit, weight|
|
|
174
263
|
digit.to_i * weight
|
|
175
264
|
end % 11
|
|
176
265
|
|
|
@@ -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
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
module BrazilianUtils
|
|
2
|
+
# Utilities for the CNS (Cartão Nacional de Saúde) number: 15 digits,
|
|
3
|
+
# definitive (starts with 1 or 2) or provisional (starts with 7, 8 or 9).
|
|
4
|
+
module CNSUtils
|
|
5
|
+
# @private
|
|
6
|
+
def self.apply_mask(digits, group_sizes, separators)
|
|
7
|
+
chunks = []
|
|
8
|
+
idx = 0
|
|
9
|
+
group_sizes.each do |size|
|
|
10
|
+
break if idx >= digits.length
|
|
11
|
+
|
|
12
|
+
chunks << digits[idx, size]
|
|
13
|
+
idx += size
|
|
14
|
+
end
|
|
15
|
+
chunks.each_with_index.map { |c, i| i.zero? ? c : "#{separators[i - 1]}#{c}" }.join
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
private_class_method :apply_mask
|
|
19
|
+
|
|
20
|
+
# @private
|
|
21
|
+
def self.weighted_sum(digits, weights)
|
|
22
|
+
digits.chars.each_with_index.sum { |d, i| d.to_i * weights[i] }
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
private_class_method :weighted_sum
|
|
26
|
+
|
|
27
|
+
# @private
|
|
28
|
+
def self.definitive_valid?(digits)
|
|
29
|
+
weights = (5..15).to_a.reverse # [15, 14, ..., 5]
|
|
30
|
+
pis = digits[0, 11]
|
|
31
|
+
|
|
32
|
+
sum = weighted_sum(pis, weights)
|
|
33
|
+
resto = sum % 11
|
|
34
|
+
dv = 11 - resto
|
|
35
|
+
dv = 0 if dv == 11
|
|
36
|
+
|
|
37
|
+
if dv == 10
|
|
38
|
+
pis2 = (pis.to_i + 1).to_s.rjust(11, '0')
|
|
39
|
+
sum2 = weighted_sum(pis2, weights)
|
|
40
|
+
resto2 = sum2 % 11
|
|
41
|
+
dv2 = 11 - resto2
|
|
42
|
+
dv2 = 0 if dv2 >= 10
|
|
43
|
+
digits == "#{pis2}001#{dv2}"
|
|
44
|
+
else
|
|
45
|
+
digits == "#{pis}000#{dv}"
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
private_class_method :definitive_valid?
|
|
50
|
+
|
|
51
|
+
# @private
|
|
52
|
+
def self.provisional_valid?(digits)
|
|
53
|
+
weights = (1..15).to_a.reverse # [15, 14, ..., 1]
|
|
54
|
+
(weighted_sum(digits, weights) % 11).zero?
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
private_class_method :provisional_valid?
|
|
58
|
+
|
|
59
|
+
# Validates a CNS number: 15 digits.
|
|
60
|
+
#
|
|
61
|
+
# @param value [String, Integer] The bare digits, or the printed
|
|
62
|
+
# 3-4-4-4 groups split by whitespace, `.`, `-` or `/`.
|
|
63
|
+
# @return [Boolean]
|
|
64
|
+
def self.is_valid(value)
|
|
65
|
+
return false unless value.is_a?(String) || value.is_a?(Integer)
|
|
66
|
+
|
|
67
|
+
raw = value.to_s.strip
|
|
68
|
+
return false unless raw.match?(%r{\A[\d\s.\-/]+\z})
|
|
69
|
+
|
|
70
|
+
digits = raw.gsub(%r{[\s.\-/]}, '')
|
|
71
|
+
return false unless digits.match?(/\A\d{15}\z/)
|
|
72
|
+
|
|
73
|
+
case digits[0]
|
|
74
|
+
when '1', '2'
|
|
75
|
+
definitive_valid?(digits)
|
|
76
|
+
when '7', '8', '9'
|
|
77
|
+
provisional_valid?(digits)
|
|
78
|
+
else
|
|
79
|
+
false
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
class << self
|
|
84
|
+
alias valid? is_valid
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
# Formats a CNS number into 3-4-4-4 groups separated by spaces.
|
|
88
|
+
#
|
|
89
|
+
# @param value [String, Integer]
|
|
90
|
+
# @param options [Hash] `:pad` left-pads with zeros to 15 digits first.
|
|
91
|
+
# @return [String]
|
|
92
|
+
def self.format(value, options = {})
|
|
93
|
+
digits = value.to_s.gsub(/\D/, '')
|
|
94
|
+
digits = digits.rjust(15, '0') if options[:pad] || options['pad']
|
|
95
|
+
return '' if digits.empty?
|
|
96
|
+
|
|
97
|
+
apply_mask(digits, [3, 4, 4, 4], [' ', ' ', ' '])
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
# Removes CNS formatting and keeps only digits, capped to 15 digits.
|
|
101
|
+
#
|
|
102
|
+
# @param value [String, Integer]
|
|
103
|
+
# @return [String]
|
|
104
|
+
def self.parse(value)
|
|
105
|
+
return '' unless value.is_a?(String) || value.is_a?(Integer)
|
|
106
|
+
|
|
107
|
+
value.to_s.gsub(/\D/, '')[0, 15]
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
end
|
|
@@ -161,7 +161,7 @@ module BrazilianUtils
|
|
|
161
161
|
#
|
|
162
162
|
# @private
|
|
163
163
|
def self.hashdigit(cpf, position)
|
|
164
|
-
val = cpf.chars.zip(position.downto(2)).sum do |digit, weight|
|
|
164
|
+
val = cpf.chars.first(position - 1).zip(position.downto(2)).sum do |digit, weight|
|
|
165
165
|
digit.to_i * weight
|
|
166
166
|
end % 11
|
|
167
167
|
|
|
@@ -188,5 +188,18 @@ module BrazilianUtils
|
|
|
188
188
|
end
|
|
189
189
|
|
|
190
190
|
private_class_method :hashdigit, :checksum
|
|
191
|
+
|
|
192
|
+
# Removes CPF formatting and keeps only digits, capped to 11 digits.
|
|
193
|
+
#
|
|
194
|
+
# @param value [String, Integer] A CPF, with or without formatting.
|
|
195
|
+
# @return [String] The parsed digits.
|
|
196
|
+
#
|
|
197
|
+
# @example
|
|
198
|
+
# parse("943.895.751-04") #=> "94389575104"
|
|
199
|
+
def self.parse(value)
|
|
200
|
+
return '' unless value.is_a?(String) || value.is_a?(Integer)
|
|
201
|
+
|
|
202
|
+
value.to_s.gsub(/\D/, '')[0, 11]
|
|
203
|
+
end
|
|
191
204
|
end
|
|
192
205
|
end
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
module BrazilianUtils
|
|
2
|
+
# Utilities for validating a payment card number (credit or debit) by its
|
|
3
|
+
# structure and Luhn check digit. No brand detection, issuer range
|
|
4
|
+
# lookup, expiry or CVV check is performed.
|
|
5
|
+
module CreditCardUtils
|
|
6
|
+
# Validates a payment card number: 12 to 19 digits and a Luhn check
|
|
7
|
+
# digit.
|
|
8
|
+
#
|
|
9
|
+
# @param value [String, Integer] Whitespace, `.`, `-` and `/` are
|
|
10
|
+
# allowed anywhere between the digits; letters make it invalid. Pass
|
|
11
|
+
# a large number as a string to avoid floating-point precision loss.
|
|
12
|
+
# @return [Boolean] A number whose digits are all the same is rejected
|
|
13
|
+
# even though it passes Luhn.
|
|
14
|
+
def self.is_valid(value)
|
|
15
|
+
return false unless value.is_a?(String) || value.is_a?(Integer)
|
|
16
|
+
|
|
17
|
+
raw = value.to_s.strip
|
|
18
|
+
return false unless raw.match?(%r{\A[\d\s.\-/]+\z})
|
|
19
|
+
|
|
20
|
+
digits = raw.gsub(%r{[\s.\-/]}, '')
|
|
21
|
+
return false unless digits.match?(/\A\d{12,19}\z/)
|
|
22
|
+
return false if digits.chars.uniq.length == 1
|
|
23
|
+
|
|
24
|
+
luhn_valid?(digits)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
class << self
|
|
28
|
+
alias valid? is_valid
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# @private
|
|
32
|
+
def self.luhn_valid?(digits)
|
|
33
|
+
sum = 0
|
|
34
|
+
digits.reverse.each_char.with_index do |ch, i|
|
|
35
|
+
d = ch.to_i
|
|
36
|
+
if i.odd?
|
|
37
|
+
d *= 2
|
|
38
|
+
d -= 9 if d > 9
|
|
39
|
+
end
|
|
40
|
+
sum += d
|
|
41
|
+
end
|
|
42
|
+
(sum % 10).zero?
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
private_class_method :luhn_valid?
|
|
46
|
+
end
|
|
47
|
+
end
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
require 'json'
|
|
2
|
+
|
|
3
|
+
module BrazilianUtils
|
|
4
|
+
# Utilities for the CSOSN (Código de Situação da Operação no Simples
|
|
5
|
+
# Nacional) table (Ajuste SINIEF 07/2005).
|
|
6
|
+
module CSOSNUtils
|
|
7
|
+
DATA_FILE = File.join(File.dirname(__FILE__), 'data', 'csosn.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
|
+
# Checks whether a value is one of the 10 official CSOSN codes.
|
|
17
|
+
#
|
|
18
|
+
# A CSOSN has no printed grouping/mask, so a value with any separator
|
|
19
|
+
# is rejected.
|
|
20
|
+
#
|
|
21
|
+
# @param value [String, Integer]
|
|
22
|
+
# @return [Boolean]
|
|
23
|
+
def self.is_valid(value)
|
|
24
|
+
return false if value.nil?
|
|
25
|
+
|
|
26
|
+
str = value.to_s.strip
|
|
27
|
+
return false unless str.match?(/\A\d{3}\z/)
|
|
28
|
+
|
|
29
|
+
load_data.any? { |row| row['code'] == str }
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
class << self
|
|
33
|
+
alias valid? is_valid
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# Looks up the description of a CSOSN code.
|
|
37
|
+
#
|
|
38
|
+
# @param value [String, Integer]
|
|
39
|
+
# @return [String, nil]
|
|
40
|
+
def self.get_description(value)
|
|
41
|
+
return nil if value.nil?
|
|
42
|
+
|
|
43
|
+
str = value.to_s.strip
|
|
44
|
+
row = load_data.find { |r| r['code'] == str }
|
|
45
|
+
row && row['description']
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# Returns all 10 official CSOSN entries.
|
|
49
|
+
#
|
|
50
|
+
# @return [Array<Hash>]
|
|
51
|
+
def self.list
|
|
52
|
+
load_data.map { |row| { code: row['code'], description: row['description'] } }
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
require 'json'
|
|
2
|
+
|
|
3
|
+
module BrazilianUtils
|
|
4
|
+
# Utilities for the CST (Código de Situação Tributária) tables: ICMS
|
|
5
|
+
# (Tabela A "origem" + Tabela B "tributação"), IPI and PIS/COFINS (which
|
|
6
|
+
# share a table).
|
|
7
|
+
module CSTUtils
|
|
8
|
+
DATA_FILE = File.join(File.dirname(__FILE__), 'data', 'cst.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.icms_origins
|
|
19
|
+
load_data['icmsOrigin']['entries'].map { |e| e['code'] }
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
# @private
|
|
23
|
+
def self.icms_csts
|
|
24
|
+
load_data['icmsCst']['entries'].map { |e| e['code'] }
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# @private
|
|
28
|
+
def self.ipi_csts
|
|
29
|
+
load_data['ipiCst']['entries'].map { |e| e['code'] }
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# @private
|
|
33
|
+
def self.pis_cofins_csts
|
|
34
|
+
load_data['pisCofinsCst']['entries'].map { |e| e['code'] }
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
private_class_method :icms_origins, :icms_csts, :ipi_csts, :pis_cofins_csts
|
|
38
|
+
|
|
39
|
+
# Checks whether a CST code is valid for a tax.
|
|
40
|
+
#
|
|
41
|
+
# @param value [String, Integer] A string or a non-negative integer. The
|
|
42
|
+
# 3-digit ICMS form (origin + Tabela B code) may have a single
|
|
43
|
+
# separator right after the origin digit (e.g. `"1-10"`). A single
|
|
44
|
+
# digit is padded to the 3-digit ICMS form; a 2-digit string is
|
|
45
|
+
# checked as a Tabela B / IPI / PIS-COFINS code.
|
|
46
|
+
# @param options [Hash] `:tax` picks the table: `"icms"`, `"ipi"`,
|
|
47
|
+
# `"pis"` or `"cofins"` (PIS and COFINS share a table). Omitted or
|
|
48
|
+
# unknown, every table is accepted.
|
|
49
|
+
# @return [Boolean]
|
|
50
|
+
def self.is_valid(value, options = {})
|
|
51
|
+
return false if value.nil?
|
|
52
|
+
|
|
53
|
+
tax = (options[:tax] || options['tax']).to_s.downcase
|
|
54
|
+
tax = nil if tax.empty? || !%w[icms ipi pis cofins].include?(tax)
|
|
55
|
+
|
|
56
|
+
raw = value.to_s.strip
|
|
57
|
+
return false if raw.empty?
|
|
58
|
+
|
|
59
|
+
three_digit, two_digit = normalize(raw)
|
|
60
|
+
|
|
61
|
+
icms_ok = !three_digit.nil? && icms_origins.include?(three_digit[0]) && icms_csts.include?(three_digit[1, 2])
|
|
62
|
+
|
|
63
|
+
case tax
|
|
64
|
+
when 'icms'
|
|
65
|
+
icms_ok
|
|
66
|
+
when 'ipi'
|
|
67
|
+
!two_digit.nil? && ipi_csts.include?(two_digit)
|
|
68
|
+
when 'pis', 'cofins'
|
|
69
|
+
!two_digit.nil? && pis_cofins_csts.include?(two_digit)
|
|
70
|
+
else
|
|
71
|
+
icms_ok ||
|
|
72
|
+
(!two_digit.nil? && (icms_csts.include?(two_digit) || ipi_csts.include?(two_digit) || pis_cofins_csts.include?(two_digit)))
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
class << self
|
|
77
|
+
alias valid? is_valid
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
# Normalizes a raw CST value into a candidate 3-digit ICMS form and/or a
|
|
81
|
+
# candidate 2-digit (Tabela B / IPI / PIS-COFINS) form.
|
|
82
|
+
#
|
|
83
|
+
# @return [Array(String, nil), Array(nil, String), Array(nil, nil)]
|
|
84
|
+
#
|
|
85
|
+
# @private
|
|
86
|
+
def self.normalize(raw)
|
|
87
|
+
case raw
|
|
88
|
+
when /\A\d\z/
|
|
89
|
+
[raw.rjust(3, '0'), nil]
|
|
90
|
+
when /\A\d{3}\z/
|
|
91
|
+
[raw, nil]
|
|
92
|
+
when /\A\d[-.]\d{2}\z/
|
|
93
|
+
[raw[0] + raw[2, 2], nil]
|
|
94
|
+
when /\A\d{2}\z/
|
|
95
|
+
[nil, raw]
|
|
96
|
+
else
|
|
97
|
+
[nil, nil]
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
private_class_method :normalize
|
|
102
|
+
end
|
|
103
|
+
end
|