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.
Files changed (58) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/ci.yml +27 -0
  3. data/README.md +446 -22
  4. data/lib/brazilian-utils/area-code-utils.rb +60 -0
  5. data/lib/brazilian-utils/bank-account-utils.rb +82 -0
  6. data/lib/brazilian-utils/bank-utils.rb +61 -0
  7. data/lib/brazilian-utils/boleto-utils.rb +139 -0
  8. data/lib/brazilian-utils/caepf-utils.rb +91 -0
  9. data/lib/brazilian-utils/cbo-utils.rb +74 -0
  10. data/lib/brazilian-utils/cei-utils.rb +79 -0
  11. data/lib/brazilian-utils/cep-utils.rb +34 -1
  12. data/lib/brazilian-utils/certidao-utils.rb +171 -0
  13. data/lib/brazilian-utils/cfop-utils.rb +74 -0
  14. data/lib/brazilian-utils/cnae-utils.rb +104 -0
  15. data/lib/brazilian-utils/cnh-utils.rb +68 -15
  16. data/lib/brazilian-utils/cno-utils.rb +76 -0
  17. data/lib/brazilian-utils/cnpj-utils.rb +126 -13
  18. data/lib/brazilian-utils/cns-utils.rb +110 -0
  19. data/lib/brazilian-utils/cpf-utils.rb +14 -1
  20. data/lib/brazilian-utils/credit-card-utils.rb +47 -0
  21. data/lib/brazilian-utils/csosn-utils.rb +55 -0
  22. data/lib/brazilian-utils/cst-utils.rb +103 -0
  23. data/lib/brazilian-utils/currency-utils.rb +347 -226
  24. data/lib/brazilian-utils/data/area_codes.json +676 -0
  25. data/lib/brazilian-utils/data/banks.json +357 -0
  26. data/lib/brazilian-utils/data/cbo.json +1 -0
  27. data/lib/brazilian-utils/data/cfop.json +1 -0
  28. data/lib/brazilian-utils/data/cnae.json +1 -0
  29. data/lib/brazilian-utils/data/csosn.json +42 -0
  30. data/lib/brazilian-utils/data/cst.json +294 -0
  31. data/lib/brazilian-utils/data/legal_nature.json +900 -0
  32. data/lib/brazilian-utils/data/municipalities.json +1 -0
  33. data/lib/brazilian-utils/data/ncm.json +1 -0
  34. data/lib/brazilian-utils/data/states.json +218 -0
  35. data/lib/brazilian-utils/date-utils.rb +504 -244
  36. data/lib/brazilian-utils/email-utils.rb +20 -9
  37. data/lib/brazilian-utils/iban-utils.rb +120 -0
  38. data/lib/brazilian-utils/ie-utils.rb +84 -0
  39. data/lib/brazilian-utils/legal-nature-utils.rb +238 -235
  40. data/lib/brazilian-utils/legal-process-utils.rb +35 -5
  41. data/lib/brazilian-utils/license-plate-utils.rb +24 -4
  42. data/lib/brazilian-utils/municipality-utils.rb +58 -0
  43. data/lib/brazilian-utils/ncm-utils.rb +91 -0
  44. data/lib/brazilian-utils/nfe-key-utils.rb +158 -0
  45. data/lib/brazilian-utils/number-utils.rb +123 -0
  46. data/lib/brazilian-utils/passport-utils.rb +65 -0
  47. data/lib/brazilian-utils/phone-utils.rb +491 -272
  48. data/lib/brazilian-utils/pis-utils.rb +16 -1
  49. data/lib/brazilian-utils/pix-key-utils.rb +70 -0
  50. data/lib/brazilian-utils/pix-payload-utils.rb +253 -0
  51. data/lib/brazilian-utils/registro-profissional-utils.rb +112 -0
  52. data/lib/brazilian-utils/renavam-utils.rb +14 -0
  53. data/lib/brazilian-utils/state-utils.rb +105 -0
  54. data/lib/brazilian-utils/text-utils.rb +105 -0
  55. data/lib/brazilian-utils/vin-utils.rb +46 -0
  56. data/lib/brazilian-utils/voter-id-utils.rb +70 -19
  57. metadata +54 -2
  58. data/.travis.yml +0 -5
@@ -1,14 +1,25 @@
1
1
  module BrazilianUtils
2
2
  module EmailUtils
3
- # Email validation pattern based on RFC 5322
4
- # This pattern validates:
5
- # - Email must not start with a dot
6
- # - Local part (before @): alphanumeric, dots, underscores, percent, plus, minus
7
- # - @ symbol required
8
- # - Domain part: alphanumeric, dots, hyphens
9
- # - Dot separator required
10
- # - TLD: at least 2 alphabetic characters
11
- EMAIL_PATTERN = /\A(?![.])[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}\z/.freeze
3
+ # Email validation pattern based on RFC 5322/RFC 1035.
4
+ #
5
+ # This is stricter than brutils/python's reference regex
6
+ # (`^(?![.])[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$`), which lets
7
+ # through addresses with a leading/trailing/doubled dot in the local
8
+ # part, or a domain label starting/ending with a hyphen. Each dot-joined
9
+ # segment is validated on its own:
10
+ # - Local part: one or more non-empty segments (letters, digits,
11
+ # `._%+-`) joined by single dots - no leading/trailing/consecutive dot
12
+ # - Domain labels: alphanumeric, may contain internal hyphens, but must
13
+ # not start or end with one (RFC 1035 "preferred name syntax")
14
+ # - TLD: at least 2 letters, no digits or hyphens
15
+ EMAIL_PATTERN = %r{
16
+ \A
17
+ [a-zA-Z0-9_%+-]+ (?: \. [a-zA-Z0-9_%+-]+ )* # local part
18
+ @
19
+ (?: [a-zA-Z0-9] (?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])? \. )+ # domain labels
20
+ [a-zA-Z]{2,63} # TLD
21
+ \z
22
+ }x.freeze
12
23
 
13
24
  # Checks if a string corresponds to a valid email address.
14
25
  #
@@ -0,0 +1,120 @@
1
+ module BrazilianUtils
2
+ # Utilities for the Brazilian IBAN: `BR` + 2 ISO 7064 MOD 97-10 check
3
+ # digits + an 8-digit bank ISPB + a 5-digit branch + a 10-digit account +
4
+ # a 1-letter account type + a 1-character owner indicator (29 characters
5
+ # total).
6
+ module IBANUtils
7
+ LENGTH = 29
8
+
9
+ # @private
10
+ def self.apply_mask(chars, group_size)
11
+ chars.chars.each_slice(group_size).map(&:join).join(' ')
12
+ end
13
+
14
+ private_class_method :apply_mask
15
+
16
+ # Removes IBAN formatting, keeps letters and digits upper-cased, capped
17
+ # to 29 characters.
18
+ #
19
+ # @param value [String, Integer]
20
+ # @return [String]
21
+ def self.parse(value)
22
+ return '' unless value.is_a?(String) || value.is_a?(Integer)
23
+
24
+ value.to_s.gsub(/[^a-zA-Z0-9]/, '').upcase[0, LENGTH]
25
+ end
26
+
27
+ # Formats an IBAN in the ISO 13616 print grouping: blocks of 4
28
+ # characters separated by spaces, upper-cased. Does not validate (use
29
+ # {is_valid}).
30
+ #
31
+ # @param value [String]
32
+ # @return [String]
33
+ def self.format(value)
34
+ cleaned = parse(value)
35
+ return '' if cleaned.empty?
36
+
37
+ apply_mask(cleaned, 4)
38
+ end
39
+
40
+ # Normalizes an IBAN candidate, honoring the printed 4-character
41
+ # grouping (a single whitespace, `.`, `-` or `/` between groups; a
42
+ # separator inside a group is rejected).
43
+ #
44
+ # @return [String, nil]
45
+ #
46
+ # @private
47
+ def self.normalize(value)
48
+ return nil unless value.is_a?(String)
49
+
50
+ raw = value.strip
51
+ return nil if raw.empty?
52
+
53
+ alnum_count = 0
54
+ raw.each_char do |ch|
55
+ if ch.match?(/[A-Za-z0-9]/)
56
+ alnum_count += 1
57
+ elsif ch.match?(%r{[\s.\-/]})
58
+ return nil unless alnum_count.positive? && (alnum_count % 4).zero?
59
+ else
60
+ return nil
61
+ end
62
+ end
63
+
64
+ compact = raw.gsub(%r{[\s.\-/]}, '').upcase
65
+ compact.match?(/\A[A-Z0-9]{#{LENGTH}}\z/) ? compact : nil
66
+ end
67
+
68
+ private_class_method :normalize
69
+
70
+ # @private
71
+ def self.checksum_valid?(compact)
72
+ rearranged = compact[4..-1] + compact[0, 4]
73
+ numeric = rearranged.chars.map { |c| c.match?(/[A-Z]/) ? (c.ord - 55).to_s : c }.join
74
+ numeric.to_i % 97 == 1
75
+ end
76
+
77
+ private_class_method :checksum_valid?
78
+
79
+ # Validates a Brazilian IBAN; any other country is invalid.
80
+ #
81
+ # @param value [String]
82
+ # @return [Boolean]
83
+ def self.is_valid(value)
84
+ compact = normalize(value)
85
+ return false unless compact
86
+ return false unless compact.start_with?('BR')
87
+ return false unless compact[2, 2].match?(/\A\d{2}\z/)
88
+ return false unless compact[4, 8].match?(/\A\d{8}\z/)
89
+ return false unless compact[12, 5].match?(/\A\d{5}\z/)
90
+ return false unless compact[17, 10].match?(/\A\d{10}\z/)
91
+ return false unless compact[27, 1].match?(/\A[A-Z]\z/)
92
+ return false unless compact[28, 1].match?(/\A[1-9A-Z]\z/)
93
+
94
+ checksum_valid?(compact)
95
+ end
96
+
97
+ class << self
98
+ alias valid? is_valid
99
+ end
100
+
101
+ # Parses a Brazilian IBAN into its fields.
102
+ #
103
+ # @param value [String]
104
+ # @return [Hash, nil] `nil` whenever {is_valid} would return false.
105
+ def self.get_info(value)
106
+ return nil unless is_valid(value)
107
+
108
+ compact = normalize(value)
109
+ {
110
+ countryCode: compact[0, 2],
111
+ checkDigits: compact[2, 2],
112
+ bankIspb: compact[4, 8],
113
+ branch: compact[12, 5],
114
+ account: compact[17, 10],
115
+ accountType: compact[27, 1],
116
+ owner: compact[28, 1]
117
+ }
118
+ end
119
+ end
120
+ end
@@ -0,0 +1,84 @@
1
+ module BrazilianUtils
2
+ # Utilities for the Inscrição Estadual (IE), the state-level tax
3
+ # registration number.
4
+ #
5
+ # @note This is deliberately a *structural-only* check: each of the 27
6
+ # Brazilian states (and the Distrito Federal) defines its own SINTEGRA
7
+ # check-digit algorithm for its IE, and the contract for this function
8
+ # does not supply a single test case exercising any of them. Without a
9
+ # verifiable reference, computing (and possibly getting wrong) 27
10
+ # different check-digit algorithms would be worse than only validating
11
+ # the one thing that is unambiguous per the contract: the expected
12
+ # digit count for each state. This mirrors the reference (Go)
13
+ # implementation, which takes the same structural-only approach for the
14
+ # same reason.
15
+ module IEUtils
16
+ # State (UF) -> accepted digit-count(s) after stripping mask
17
+ # characters ({space, '.', '-', '/'}).
18
+ DIGIT_COUNTS = {
19
+ 'AC' => [13],
20
+ 'AL' => [9],
21
+ 'AM' => [9],
22
+ 'AP' => [9],
23
+ 'BA' => [8, 9],
24
+ 'CE' => [9],
25
+ 'DF' => [13],
26
+ 'ES' => [9],
27
+ 'GO' => [9],
28
+ 'MA' => [9],
29
+ 'MG' => [13],
30
+ 'MS' => [9],
31
+ 'MT' => [11],
32
+ 'PA' => [9],
33
+ 'PB' => [9],
34
+ 'PE' => [9, 14],
35
+ 'PI' => [9],
36
+ 'PR' => [10],
37
+ 'RJ' => [8],
38
+ 'RN' => [9, 10],
39
+ 'RO' => [9, 14],
40
+ 'RR' => [9],
41
+ 'RS' => [10],
42
+ 'SC' => [9],
43
+ 'SE' => [9],
44
+ 'SP' => [12],
45
+ 'TO' => [9, 11]
46
+ }.freeze
47
+
48
+ # Checks the *structure* of a state Inscrição Estadual: whether its
49
+ # cleaned length matches one of the digit counts accepted by the given
50
+ # UF. This never validates a per-state SINTEGRA check digit.
51
+ #
52
+ # @param value [String, Integer] The IE, bare or separated by any mix
53
+ # of space, `.`, `-` or `/`. For São Paulo, a value starting with
54
+ # `P`/`p` (a produtor rural registration) is accepted when its
55
+ # cleaned length is exactly 13.
56
+ # @param state [String] The two-letter UF code (case-insensitive).
57
+ # @return [Boolean] false for an unknown UF or empty/invalid input.
58
+ def self.is_valid(value, state)
59
+ return false unless value.is_a?(String) || value.is_a?(Integer)
60
+
61
+ uf = state.to_s.strip.upcase
62
+ counts = DIGIT_COUNTS[uf]
63
+ return false unless counts
64
+
65
+ raw = value.to_s.strip
66
+ return false if raw.empty?
67
+
68
+ cleaned = raw.gsub(%r{[\s.\-/]}, '')
69
+ return false if cleaned.empty?
70
+
71
+ if uf == 'SP' && cleaned.start_with?('P', 'p')
72
+ return cleaned.length == 13
73
+ end
74
+
75
+ return false unless cleaned.match?(/\A\d+\z/)
76
+
77
+ counts.include?(cleaned.length)
78
+ end
79
+
80
+ class << self
81
+ alias valid? is_valid
82
+ end
83
+ end
84
+ end