cnpj-dv 0.0.1 β†’ 2.0.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 4715b7cec54902ddb19fba13cb6ed707dfe47aa621857a61ca0fe8beacf3ac70
4
- data.tar.gz: bee5b04815a43097528464af159dbdbb54a6288c8f3e2491ff585dd0467ca4c1
3
+ metadata.gz: 8d6109a8bd760c61186dc99ec5503bb42880d9ebb3c19745962ba5a0d97c6cfc
4
+ data.tar.gz: 47242e1f246d23ebab4d8c3be97d7d7b46060c974e011ad0412a93595fb69767
5
5
  SHA512:
6
- metadata.gz: 103dfbad0358767ffd771c187b336605672a0d3e7e9633a3c98e143c3a6717f735a46eb1de466f709e77d3abb94bb65cd791892a475ab9940ff2b62705324752
7
- data.tar.gz: faa45d622b815d48d6b51d95e41b300eb989e6ebb2da5f4c8a85b74162f085e09fcda6c7c8347dc238b787ed14dd90801c18115d29e171ca442996c052696015
6
+ metadata.gz: 85686458b90a155cf2263ef4517f66732efbead21d29cc6683827ad2fae6a0f6e3949b17bbbbd2cd1a842c5cd1ff57a827e6ec1b10bc8cdb451d910ab954f383
7
+ data.tar.gz: 11dbfd7b80b6f25695678bc0a5e8804b08ae17fbbc7cf24aea2071f717092ebbc4e79db8945ed66d02bceef6e5f4ea467a8e7cd65c679c02381b087fd751b921
data/CHANGELOG.md ADDED
@@ -0,0 +1,30 @@
1
+ # cnpj-dv
2
+
3
+ ## 2.0.0
4
+
5
+ ### πŸŽ‰ v2 at a glance 🎊
6
+
7
+ - **Error hierarchy**: Aligns with the monorepo standard β€” API misuse vs domain errors, `CnpjDV::Error` marker, and native-compatible rescue.
8
+
9
+ ### BREAKING CHANGES
10
+
11
+ - **Error classes**: Removed `CnpjCheckDigitsTypeError`, `CnpjCheckDigitsInputTypeError`, `CnpjCheckDigitsException`, `CnpjCheckDigitsInputLengthException`, and `CnpjCheckDigitsInputInvalidException`.
12
+ - **Migration map**: `InputTypeError` β†’ `TypeMismatchError`; `InputLengthException` β†’ `InvalidLengthError`; `InputInvalidException` β†’ `ValidationError`.
13
+ - **Rescue targets**: `rescue CnpjCheckDigitsException` no longer applies; use `rescue CnpjDV::Error` for a library-wide catch, or `rescue DomainError` for `InvalidLengthError` and `ValidationError`.
14
+ - **Docs**: READMEs now document misuse vs domain categories, per-class rescue guidance, and four rescue granularity levels β€” see [README](./README.md).
15
+
16
+ ## 1.0.0
17
+
18
+ ### πŸš€ Stable Version Released!
19
+
20
+ Utility class to calculate check digits on CNPJ (Brazilian Business Tax ID). Main features:
21
+
22
+ - **Flexible input**: Accepts a `String` or `Array` of strings (formatted or raw).
23
+ - **Format agnostic**: Automatically strips non-alphanumeric characters and uppercases letters before processing.
24
+ - **Alphanumeric CNPJ**: Supports letters `A–Z` in the base; check digits remain numeric via modulo-11.
25
+ - **Lazy evaluation**: `CnpjDV::CnpjCheckDigits` computes `first`, `second`, `both`, and `cnpj` only on first access, then caches.
26
+ - **Eligibility gates**: Rejects wrong types, invalid length, all-zero base/branch IDs, and repeated numeric digits.
27
+ - **Error handling**: `CnpjCheckDigitsInputTypeError` (`TypeError`) plus length/invalid `StandardError` subclasses with structured attributes.
28
+ - **Minimal dependencies**: Requires `lacus-utils` for reusable utilities.
29
+
30
+ For detailed usage and API reference, see the [README](./README.md).
data/README.md ADDED
@@ -0,0 +1,306 @@
1
+ cnpj-dv for Ruby
2
+
3
+ [Gem Version](https://rubygems.org/gems/cnpj-dv)
4
+ [Downloads Count](https://rubygems.org/gems/cnpj-dv)
5
+ [Ruby Version](https://www.ruby-lang.org/)
6
+ [Test Status](https://github.com/LacusSolutions/br-utils-ruby/actions)
7
+ [Last Update Date](https://github.com/LacusSolutions/br-utils-ruby)
8
+ [Project License](https://github.com/LacusSolutions/br-utils-ruby/blob/main/LICENSE)
9
+
10
+ > πŸš€ **Full support for the [new alphanumeric CNPJ format](https://github.com/user-attachments/files/23937961/calculodvcnpjalfanaumerico.pdf).**
11
+
12
+ > 🌎 [Acessar documentaΓ§Γ£o em portuguΓͺs](https://github.com/LacusSolutions/br-utils-ruby/blob/main/packages/cnpj-dv/README.pt.md)
13
+
14
+ A Ruby utility to calculate check digits on CNPJ (Brazilian Business Tax ID).
15
+
16
+ ## Ruby Support
17
+
18
+
19
+ | Ruby 3.2 | Ruby 3.3 | Ruby 3.4 |
20
+ | --------- | --------- | --------- |
21
+ | Passing βœ” | Passing βœ” | Passing βœ” |
22
+
23
+
24
+
25
+
26
+ ## Features
27
+
28
+ - βœ… **Alphanumeric CNPJ**: Full support for the new alphanumeric CNPJ format (introduced in 2026)
29
+ - βœ… **Flexible input**: Accepts `String` or `Array` of strings
30
+ - βœ… **Format agnostic**: Strips non-alphanumeric characters from string input and uppercases letters
31
+ - βœ… **Auto-expansion**: Multi-character strings in arrays are joined and parsed like a single string
32
+ - βœ… **Input validation**: Rejects ineligible CNPJs (all-zero base ID `00000000`, all-zero branch `0000`, or 12 numeric-only repeated digits)
33
+ - βœ… **Lazy evaluation**: Check digits are calculated only when accessed (via methods)
34
+ - βœ… **Caching**: Calculated values are cached for subsequent access
35
+ - βœ… **Minimal dependencies**: Only `[lacus-utils](https://rubygems.org/gems/lacus-utils)`
36
+ - βœ… **Error handling**: API misuse vs domain errors with a `CnpjDV::Error` marker for library-wide rescue
37
+
38
+
39
+
40
+ ## Installation
41
+
42
+ Install the gem directly:
43
+
44
+ ```bash
45
+ gem install cnpj-dv
46
+ ```
47
+
48
+ Or add it to your `Gemfile` and run `bundle install`:
49
+
50
+ ```ruby
51
+ gem 'cnpj-dv'
52
+ ```
53
+
54
+
55
+
56
+ ## Require
57
+
58
+ ```ruby
59
+ require 'cnpj-dv'
60
+ ```
61
+
62
+
63
+
64
+ ## Quick Start
65
+
66
+ ```ruby
67
+ require 'cnpj-dv'
68
+
69
+ check_digits = CnpjDV::CnpjCheckDigits.new('914157320007')
70
+
71
+ check_digits.first # => '9'
72
+ check_digits.second # => '3'
73
+ check_digits.both # => '93'
74
+ check_digits.cnpj # => '91415732000793'
75
+ ```
76
+
77
+ With alphanumeric CNPJ (new format):
78
+
79
+ ```ruby
80
+ require 'cnpj-dv'
81
+
82
+ check_digits = CnpjDV::CnpjCheckDigits.new('MGKGMJ9X0001')
83
+
84
+ check_digits.first # => '6'
85
+ check_digits.second # => '8'
86
+ check_digits.both # => '68'
87
+ check_digits.cnpj # => 'MGKGMJ9X000168'
88
+ ```
89
+
90
+
91
+
92
+ ## Usage
93
+
94
+ The main resource of this package is the class `CnpjDV::CnpjCheckDigits`. Through an instance, you access CNPJ check-digit information:
95
+
96
+ - `initialize`: `CnpjDV::CnpjCheckDigits.new(cnpj_input)` β€” `cnpj_input` must be a `String` or an `Array` of strings. After sanitization, the value must have 12–14 alphanumeric characters (formatting stripped from strings; letters uppercased). Only the **first 12** characters are used as the base; if you pass 13 or 14 characters (e.g. a full CNPJ including prior check digits), characters 13–14 are **ignored** and the digits are recalculated. There are **no options**, keyword arguments, or configuration objects β€” the constructor takes only the CNPJ input.
97
+ - `first`: First check digit (13th character of the full CNPJ). Lazy, cached.
98
+ - `second`: Second check digit (14th character of the full CNPJ). Lazy, cached.
99
+ - `both`: Both check digits concatenated as a string.
100
+ - `cnpj`: The complete CNPJ as a string of 14 characters (12 base characters + 2 check digits).
101
+
102
+
103
+
104
+ ### Input formats
105
+
106
+ The `CnpjCheckDigits` class accepts multiple input formats:
107
+
108
+ **String input:** raw digits and/or letters, or formatted CNPJ (e.g. `91.415.732/0007-93`, `MG.KGM.J9X/0001-68`). Non-alphanumeric characters are removed; lowercase letters are uppercased.
109
+
110
+ **Array of strings:** each element must be a string; values are concatenated and then parsed like a single string (e.g. `['9','1','4',…]`, `['9141','5732','0007']`, `['MG','KGM','J9X','0001']`). Non-string elements are not allowed.
111
+
112
+ ```ruby
113
+ require 'cnpj-dv'
114
+
115
+ # String β€” plain, formatted, or with existing check digits (only first 12 chars used)
116
+ CnpjDV::CnpjCheckDigits.new('914157320007')
117
+ CnpjDV::CnpjCheckDigits.new('91.415.732/0007')
118
+ CnpjDV::CnpjCheckDigits.new('91415732000793')
119
+
120
+ # Array of strings β€” single- or multi-character elements
121
+ CnpjDV::CnpjCheckDigits.new(%w[9 1 4 1 5 7 3 2 0 0 0 7])
122
+ CnpjDV::CnpjCheckDigits.new(%w[9141 5732 0007])
123
+ CnpjDV::CnpjCheckDigits.new(%w[MG KGM J9X 0001])
124
+ ```
125
+
126
+
127
+
128
+ ### Error handling
129
+
130
+ Errors fall into two categories:
131
+
132
+ | Category | Meaning |
133
+ |---|---|
134
+ | **API misuse** | The caller invoked the library incorrectly (wrong type). Detectable from the call shape. |
135
+ | **Domain error** | The call was structurally correct, but a value violates a business rule (length, eligibility, format). |
136
+
137
+ Every custom error includes the `CnpjDV::Error` marker module. Domain failures (`InvalidLengthError`, `ValidationError`) inherit from `CnpjDV::DomainError` (`RangeError`).
138
+
139
+ #### Summary
140
+
141
+ | Class | Inherits from | Category | Trigger condition |
142
+ |---|---|---|---|
143
+ | `CnpjDV::TypeMismatchError` | `TypeError` (+ `include Error`) | API misuse | Argument has the wrong data type |
144
+ | `CnpjDV::InvalidLengthError` | `CnpjDV::DomainError` | Domain error | Sanitized length is not 12–14 |
145
+ | `CnpjDV::ValidationError` | `CnpjDV::DomainError` | Domain error | Ineligible base/branch ID or repeated numeric digits |
146
+
147
+ #### `CnpjDV::Error` (marker module)
148
+
149
+ - **Inheritance:** module marker mixed into every library error via `include` (not a class).
150
+ - **Category:** N/A (rescue target only) β€” not a failure mode by itself.
151
+ - **When it is raised:** Never raised directly; included by every custom error the library raises.
152
+ - **Example:** N/A
153
+ - **How to rescue it:**
154
+
155
+ ```ruby
156
+ rescue CnpjDV::Error
157
+ # everything this library raises
158
+ ```
159
+
160
+ #### `CnpjDV::DomainError`
161
+
162
+ - **Inheritance:** `CnpjDV::DomainError < RangeError` (includes `CnpjDV::Error`)
163
+ - **Category:** Domain error β€” ancestor for numeric/length domain failures.
164
+ - **When it is raised:** Not raised directly; prefer raising a leaf subclass.
165
+ - **Example:** Prefer `raise CnpjDV::InvalidLengthError` over raising `DomainError` directly.
166
+ - **How to rescue it:**
167
+
168
+ ```ruby
169
+ rescue CnpjDV::DomainError
170
+ # InvalidLengthError, ValidationError, and any other DomainError subclass
171
+ ```
172
+
173
+ #### `CnpjDV::TypeMismatchError`
174
+
175
+ - **Inheritance:** `CnpjDV::TypeMismatchError < TypeError` (includes `CnpjDV::Error`)
176
+ - **Category:** API misuse β€” the caller passed a value of the wrong type.
177
+ - **When it is raised:** Raised when the CNPJ input is not a `String` or an `Array` of strings (or an array contains a non-string element).
178
+ - **Example:**
179
+
180
+ ```ruby
181
+ CnpjDV::CnpjCheckDigits.new(12_345_678_000_100) # raises CnpjDV::TypeMismatchError
182
+ ```
183
+
184
+ - **How to rescue it:**
185
+
186
+ ```ruby
187
+ rescue CnpjDV::TypeMismatchError
188
+ # this library's type-contract violation
189
+
190
+ rescue TypeError
191
+ # native type errors, including this library's TypeMismatchError
192
+ ```
193
+
194
+ #### `CnpjDV::InvalidLengthError`
195
+
196
+ - **Inheritance:** `CnpjDV::InvalidLengthError < CnpjDV::DomainError < RangeError` (includes `CnpjDV::Error`)
197
+ - **Category:** Domain error β€” a collection or string length violates a business rule.
198
+ - **When it is raised:** Raised when the sanitized CNPJ input does not contain 12 to 14 alphanumeric characters.
199
+ - **Example:**
200
+
201
+ ```ruby
202
+ CnpjDV::CnpjCheckDigits.new('12345678901') # raises CnpjDV::InvalidLengthError
203
+ ```
204
+
205
+ - **How to rescue it:**
206
+
207
+ ```ruby
208
+ rescue CnpjDV::InvalidLengthError
209
+ # this exact length violation
210
+
211
+ rescue CnpjDV::DomainError
212
+ # RangeError-rooted domain failures from this library
213
+ ```
214
+
215
+ #### `CnpjDV::ValidationError`
216
+
217
+ - **Inheritance:** `CnpjDV::ValidationError < CnpjDV::DomainError < RangeError` (includes `CnpjDV::Error`)
218
+ - **Category:** Domain error β€” a value fails a non-numeric, non-length domain rule.
219
+ - **When it is raised:** Raised when the base ID is `00000000`, the branch ID is `0000`, or the first 12 characters are the same numeric digit.
220
+ - **Example:**
221
+
222
+ ```ruby
223
+ CnpjDV::CnpjCheckDigits.new('000000000001') # raises CnpjDV::ValidationError
224
+ ```
225
+
226
+ - **How to rescue it:**
227
+
228
+ ```ruby
229
+ rescue CnpjDV::ValidationError
230
+ # this exact domain validation failure
231
+
232
+ rescue CnpjDV::DomainError
233
+ # RangeError-rooted domain failures from this library
234
+ ```
235
+
236
+ #### Rescue granularity
237
+
238
+ ```ruby
239
+ # 1) Single native class β€” catches type misuse from this library (and other TypeErrors).
240
+ rescue TypeError
241
+ # CnpjDV::TypeMismatchError and any other TypeError (library or not)
242
+
243
+ # 2) CnpjDV::DomainError β€” catches business-rule violations under DomainError.
244
+ rescue CnpjDV::DomainError
245
+ # CnpjDV::InvalidLengthError, CnpjDV::ValidationError, and other DomainError subclasses
246
+
247
+ # 3) CnpjDV::Error β€” catches everything the library raises.
248
+ rescue CnpjDV::Error
249
+ # every custom error that includes CnpjDV::Error
250
+
251
+ # 4) Specific leaf class β€” catches only that exact failure mode.
252
+ rescue CnpjDV::InvalidLengthError
253
+ # only CnpjDV::InvalidLengthError
254
+ ```
255
+
256
+ Notable attributes on raised errors:
257
+
258
+ - `TypeMismatchError`: `actual_input`, `actual_type`, `expected_type`
259
+ - `InvalidLengthError`: `actual_input`, `evaluated_input`, `min_expected_length`, `max_expected_length`
260
+ - `ValidationError`: `actual_input`, `reason`
261
+
262
+
263
+
264
+ ### Other available resources
265
+
266
+ After `require 'cnpj-dv'`:
267
+
268
+ - `CnpjDV::CNPJ_MIN_LENGTH`: `12`
269
+ - `CnpjDV::CNPJ_MAX_LENGTH`: `14`
270
+ - **Errors**: see above (`CnpjDV::Error`, `DomainError`, and raised leaves)
271
+
272
+
273
+
274
+ ## Calculation algorithm
275
+
276
+ The package computes check digits with the official Brazilian modulo-11 rules extended to alphanumeric characters:
277
+
278
+ 1. **Character value:** each character contributes `ord(character) βˆ’ 48` (so `0`–`9` stay 0–9; letters use their ASCII offset from `0`).
279
+ 2. **Weights:** from **right to left**, multiply by weights that cycle **2, 3, 4, 5, 6, 7, 8, 9**, then repeat from 2.
280
+ 3. **First check digit (13th position):** apply steps 1–2 to the first **12** base characters; let `r = sum % 11`. The digit is `0` if `r < 2`, otherwise `11 βˆ’ r`.
281
+ 4. **Second check digit (14th position):** apply steps 1–2 to the first 12 characters **plus** the first check digit; same formula for `r`.
282
+
283
+
284
+
285
+ ## Contribution & Support
286
+
287
+ We welcome contributions! Please see our [Contributing Guidelines](https://github.com/LacusSolutions/br-utils-ruby/blob/main/CONTRIBUTING.md) for details. If you find this project helpful, please consider:
288
+
289
+ - ⭐ Starring the repository
290
+ - 🀝 Contributing to the codebase
291
+ - πŸ’‘ [Suggesting new features](https://github.com/LacusSolutions/br-utils-ruby/issues)
292
+ - πŸ› [Reporting bugs](https://github.com/LacusSolutions/br-utils-ruby/issues)
293
+
294
+
295
+
296
+ ## License
297
+
298
+ This project is licensed under the MIT License β€” see the [LICENSE](https://github.com/LacusSolutions/br-utils-ruby/blob/main/LICENSE) file for details.
299
+
300
+ ## Changelog
301
+
302
+ See [CHANGELOG](./CHANGELOG.md) for a list of changes and version history.
303
+
304
+ ---
305
+
306
+ Made with ❀️ by [Lacus Solutions](https://github.com/LacusSolutions)
data/README.pt.md ADDED
@@ -0,0 +1,269 @@
1
+ ![cnpj-dv para Ruby](https://br-utils.vercel.app/img/cover_cnpj-dv.jpg)
2
+
3
+ > πŸš€ **Suporte total ao [novo formato alfanumΓ©rico de CNPJ](https://github.com/user-attachments/files/23937961/calculodvcnpjalfanaumerico.pdf).**
4
+
5
+ > 🌎 [Access documentation in English](https://github.com/LacusSolutions/br-utils-ruby/blob/main/packages/cnpj-dv/README.md)
6
+
7
+ UtilitΓ‘rio em Ruby para calcular os dΓ­gitos verificadores de CNPJ (Cadastro Nacional da Pessoa JurΓ­dica).
8
+
9
+ ## Recursos
10
+
11
+ - βœ… **CNPJ alfanumΓ©rico**: Suporte completo ao novo formato alfanumΓ©rico de CNPJ (a partir de 2026)
12
+ - βœ… **Entrada flexΓ­vel**: Aceita `String` ou `Array` de strings
13
+ - βœ… **AgnΓ³stico ao formato**: Remove caracteres nΓ£o alfanumΓ©ricos da entrada em string e converte letras para maiΓΊsculas
14
+ - βœ… **JunΓ§Γ£o em array**: Strings com vΓ‘rios caracteres em arrays sΓ£o concatenadas e interpretadas como uma ΓΊnica sequΓͺncia
15
+ - βœ… **ValidaΓ§Γ£o de entrada**: Rejeita CNPJs inelegΓ­veis (base toda zero `00000000`, filial `0000`, ou 12 dΓ­gitos numΓ©ricos repetidos)
16
+ - βœ… **AvaliaΓ§Γ£o lazy**: DΓ­gitos verificadores sΓ£o calculados apenas quando acessados (via mΓ©todos)
17
+ - βœ… **Cache**: Valores calculados sΓ£o armazenados em cache para acessos subsequentes
18
+ - βœ… **DependΓͺncias mΓ­nimas**: Apenas [`lacus-utils`](https://rubygems.org/gems/lacus-utils)
19
+ - βœ… **Tratamento de erros**: Erros de uso da API vs erros de domΓ­nio, com o marcador `CnpjDV::Error` para resgate em nΓ­vel de biblioteca
20
+
21
+ ## InstalaΓ§Γ£o
22
+
23
+ Instale a gem diretamente:
24
+
25
+ ```bash
26
+ gem install cnpj-dv
27
+ ```
28
+
29
+ Ou adicione ao seu `Gemfile` e execute `bundle install`:
30
+
31
+ ```ruby
32
+ gem 'cnpj-dv'
33
+ ```
34
+
35
+ ## Require
36
+
37
+ ```ruby
38
+ require 'cnpj-dv'
39
+ ```
40
+
41
+ ## InΓ­cio rΓ‘pido
42
+
43
+ ```ruby
44
+ require 'cnpj-dv'
45
+
46
+ check_digits = CnpjDV::CnpjCheckDigits.new('914157320007')
47
+
48
+ check_digits.first # => '9'
49
+ check_digits.second # => '3'
50
+ check_digits.both # => '93'
51
+ check_digits.cnpj # => '91415732000793'
52
+ ```
53
+
54
+ Com CNPJ alfanumΓ©rico (novo formato):
55
+
56
+ ```ruby
57
+ require 'cnpj-dv'
58
+
59
+ check_digits = CnpjDV::CnpjCheckDigits.new('MGKGMJ9X0001')
60
+
61
+ check_digits.first # => '6'
62
+ check_digits.second # => '8'
63
+ check_digits.both # => '68'
64
+ check_digits.cnpj # => 'MGKGMJ9X000168'
65
+ ```
66
+
67
+ ## UtilizaΓ§Γ£o
68
+
69
+ O principal recurso deste pacote Γ© a classe `CnpjDV::CnpjCheckDigits`. Por meio da instΓ’ncia, vocΓͺ acessa as informaΓ§Γ΅es dos dΓ­gitos verificadores do CNPJ:
70
+
71
+ - **`initialize`**: `CnpjDV::CnpjCheckDigits.new(cnpj_input)` β€” `cnpj_input` deve ser `String` ou `Array` de strings. ApΓ³s a sanitizaΓ§Γ£o, o valor deve ter 12–14 caracteres alfanumΓ©ricos (formataΓ§Γ£o removida em strings; letras em maiΓΊsculas). Apenas os **primeiros 12** caracteres entram como base; com 13 ou 14 caracteres (ex.: CNPJ completo com DV anteriores), os caracteres 13 e 14 sΓ£o **ignorados** e os dΓ­gitos sΓ£o recalculados. NΓ£o hΓ‘ **opΓ§Γ΅es**, argumentos nomeados nem objeto de configuraΓ§Γ£o β€” o construtor recebe apenas a entrada de CNPJ.
72
+ - **`first`**: Primeiro dΓ­gito verificador (13ΒΊ caractere do CNPJ completo). Lazy, em cache.
73
+ - **`second`**: Segundo dΓ­gito verificador (14ΒΊ caractere do CNPJ completo). Lazy, em cache.
74
+ - **`both`**: Ambos os dΓ­gitos verificadores concatenados em uma string.
75
+ - **`cnpj`**: O CNPJ completo como string de 14 caracteres (12 da base + 2 dΓ­gitos verificadores).
76
+
77
+ ### Formatos de entrada
78
+
79
+ A classe `CnpjCheckDigits` aceita mΓΊltiplos formatos de entrada:
80
+
81
+ **String:** dΓ­gitos e/ou letras crus, ou CNPJ formatado (ex.: `91.415.732/0007-93`, `MG.KGM.J9X/0001-68`). Caracteres nΓ£o alfanumΓ©ricos sΓ£o removidos; letras minΓΊsculas viram maiΓΊsculas.
82
+
83
+ **Array de strings:** cada elemento deve ser string; os valores sΓ£o concatenados e interpretados como uma ΓΊnica string (ex.: `['9','1','4',…]`, `['9141','5732','0007']`, `['MG','KGM','J9X','0001']`). Elementos que nΓ£o sΓ£o strings nΓ£o sΓ£o permitidos.
84
+
85
+ ```ruby
86
+ require 'cnpj-dv'
87
+
88
+ # String β€” crua, formatada ou com DV existentes (apenas os 12 primeiros caracteres sΓ£o usados)
89
+ CnpjDV::CnpjCheckDigits.new('914157320007')
90
+ CnpjDV::CnpjCheckDigits.new('91.415.732/0007')
91
+ CnpjDV::CnpjCheckDigits.new('91415732000793')
92
+
93
+ # Array de strings β€” elementos de um ou vΓ‘rios caracteres
94
+ CnpjDV::CnpjCheckDigits.new(%w[9 1 4 1 5 7 3 2 0 0 0 7])
95
+ CnpjDV::CnpjCheckDigits.new(%w[9141 5732 0007])
96
+ CnpjDV::CnpjCheckDigits.new(%w[MG KGM J9X 0001])
97
+ ```
98
+
99
+ ### Tratamento de erros
100
+
101
+ Os erros se dividem em duas categorias:
102
+
103
+ | Categoria | Significado |
104
+ |---|---|
105
+ | **Uso incorreto da API** | O chamador usou a biblioteca de forma incorreta (tipo errado). DetectΓ‘vel pela forma da chamada. |
106
+ | **Erro de domΓ­nio** | A chamada estava estruturalmente correta, mas um valor viola uma regra de negΓ³cio (tamanho, elegibilidade, formato). |
107
+
108
+ Todo erro customizado inclui o mΓ³dulo marcador `CnpjDV::Error`. Falhas de domΓ­nio (`InvalidLengthError`, `ValidationError`) herdam de `CnpjDV::DomainError` (`RangeError`).
109
+
110
+ #### Resumo
111
+
112
+ | Classe | Herda de | Categoria | CondiΓ§Γ£o de disparo |
113
+ |---|---|---|---|
114
+ | `CnpjDV::TypeMismatchError` | `TypeError` (+ `include Error`) | Uso incorreto da API | Argumento com tipo de dado incorreto |
115
+ | `CnpjDV::InvalidLengthError` | `CnpjDV::DomainError` | Erro de domΓ­nio | Tamanho apΓ³s sanitizaΓ§Γ£o nΓ£o Γ© 12–14 |
116
+ | `CnpjDV::ValidationError` | `CnpjDV::DomainError` | Erro de domΓ­nio | Base/filial inelegΓ­vel ou dΓ­gitos numΓ©ricos repetidos |
117
+
118
+ #### `CnpjDV::Error` (mΓ³dulo marcador)
119
+
120
+ - **HeranΓ§a:** mΓ³dulo marcador misturado em todo erro da biblioteca via `include` (nΓ£o Γ© uma classe).
121
+ - **Categoria:** N/A (apenas alvo de `rescue`) β€” nΓ£o Γ© um modo de falha por si sΓ³.
122
+ - **Quando Γ© levantado:** Nunca diretamente; incluΓ­do por todo erro customizado que a biblioteca levanta.
123
+ - **Exemplo:** N/A
124
+ - **Como resgatar:**
125
+
126
+ ```ruby
127
+ rescue CnpjDV::Error
128
+ # tudo o que esta biblioteca levanta
129
+ ```
130
+
131
+ #### `CnpjDV::DomainError`
132
+
133
+ - **HeranΓ§a:** `CnpjDV::DomainError < RangeError` (inclui `CnpjDV::Error`)
134
+ - **Categoria:** Erro de domΓ­nio β€” ancestral das falhas numΓ©ricas/de tamanho.
135
+ - **Quando Γ© levantado:** NΓ£o Γ© levantado diretamente; prefira uma subclasse folha.
136
+ - **Exemplo:** Prefira `raise CnpjDV::InvalidLengthError` a levantar `DomainError` diretamente.
137
+ - **Como resgatar:**
138
+
139
+ ```ruby
140
+ rescue CnpjDV::DomainError
141
+ # InvalidLengthError, ValidationError e qualquer outra subclasse de DomainError
142
+ ```
143
+
144
+ #### `CnpjDV::TypeMismatchError`
145
+
146
+ - **HeranΓ§a:** `CnpjDV::TypeMismatchError < TypeError` (inclui `CnpjDV::Error`)
147
+ - **Categoria:** Uso incorreto da API β€” o chamador passou um valor do tipo errado.
148
+ - **Quando Γ© levantado:** Levantado quando a entrada de CNPJ nΓ£o Γ© `String` nem `Array` de strings (ou o array contΓ©m elemento que nΓ£o Γ© string).
149
+ - **Exemplo:**
150
+
151
+ ```ruby
152
+ CnpjDV::CnpjCheckDigits.new(12_345_678_000_100) # levanta CnpjDV::TypeMismatchError
153
+ ```
154
+
155
+ - **Como resgatar:**
156
+
157
+ ```ruby
158
+ rescue CnpjDV::TypeMismatchError
159
+ # violaΓ§Γ£o de contrato de tipo desta biblioteca
160
+
161
+ rescue TypeError
162
+ # erros nativos de tipo, incluindo TypeMismatchError desta biblioteca
163
+ ```
164
+
165
+ #### `CnpjDV::InvalidLengthError`
166
+
167
+ - **HeranΓ§a:** `CnpjDV::InvalidLengthError < CnpjDV::DomainError < RangeError` (inclui `CnpjDV::Error`)
168
+ - **Categoria:** Erro de domΓ­nio β€” o tamanho de uma coleΓ§Γ£o ou string viola uma regra de negΓ³cio.
169
+ - **Quando Γ© levantado:** Levantado quando a entrada de CNPJ sanitizada nΓ£o contΓ©m de 12 a 14 caracteres alfanumΓ©ricos.
170
+ - **Exemplo:**
171
+
172
+ ```ruby
173
+ CnpjDV::CnpjCheckDigits.new('12345678901') # levanta CnpjDV::InvalidLengthError
174
+ ```
175
+
176
+ - **Como resgatar:**
177
+
178
+ ```ruby
179
+ rescue CnpjDV::InvalidLengthError
180
+ # esta violaΓ§Γ£o exata de tamanho
181
+
182
+ rescue CnpjDV::DomainError
183
+ # falhas de domΓ­nio enraizadas em RangeError desta biblioteca
184
+ ```
185
+
186
+ #### `CnpjDV::ValidationError`
187
+
188
+ - **HeranΓ§a:** `CnpjDV::ValidationError < CnpjDV::DomainError < RangeError` (inclui `CnpjDV::Error`)
189
+ - **Categoria:** Erro de domΓ­nio β€” um valor falha uma regra de domΓ­nio que nΓ£o Γ© numΓ©rica nem de tamanho.
190
+ - **Quando Γ© levantado:** Levantado quando a base Γ© `00000000`, a filial Γ© `0000`, ou os 12 primeiros caracteres sΓ£o o mesmo dΓ­gito numΓ©rico.
191
+ - **Exemplo:**
192
+
193
+ ```ruby
194
+ CnpjDV::CnpjCheckDigits.new('000000000001') # levanta CnpjDV::ValidationError
195
+ ```
196
+
197
+ - **Como resgatar:**
198
+
199
+ ```ruby
200
+ rescue CnpjDV::ValidationError
201
+ # esta falha exata de validaΓ§Γ£o de domΓ­nio
202
+
203
+ rescue CnpjDV::DomainError
204
+ # falhas de domΓ­nio enraizadas em RangeError desta biblioteca
205
+ ```
206
+
207
+ #### Granularidade de rescue
208
+
209
+ ```ruby
210
+ # 1) Uma classe nativa β€” captura uso incorreto de tipo desta biblioteca (e outros TypeError).
211
+ rescue TypeError
212
+ # CnpjDV::TypeMismatchError e qualquer outro TypeError (da biblioteca ou nΓ£o)
213
+
214
+ # 2) CnpjDV::DomainError β€” captura violaΓ§Γ΅es de regra de negΓ³cio sob DomainError.
215
+ rescue CnpjDV::DomainError
216
+ # CnpjDV::InvalidLengthError, CnpjDV::ValidationError e outras subclasses de DomainError
217
+
218
+ # 3) CnpjDV::Error β€” captura tudo o que a biblioteca levanta.
219
+ rescue CnpjDV::Error
220
+ # todo erro customizado que inclui CnpjDV::Error
221
+
222
+ # 4) Classe folha especΓ­fica β€” captura apenas aquele modo de falha.
223
+ rescue CnpjDV::InvalidLengthError
224
+ # apenas CnpjDV::InvalidLengthError
225
+ ```
226
+
227
+ Atributos relevantes nos erros:
228
+
229
+ - `TypeMismatchError`: `actual_input`, `actual_type`, `expected_type`
230
+ - `InvalidLengthError`: `actual_input`, `evaluated_input`, `min_expected_length`, `max_expected_length`
231
+ - `ValidationError`: `actual_input`, `reason`
232
+
233
+ ### Outros recursos disponΓ­veis
234
+
235
+ ApΓ³s `require 'cnpj-dv'`:
236
+
237
+ - **`CnpjDV::CNPJ_MIN_LENGTH`**: `12`
238
+ - **`CnpjDV::CNPJ_MAX_LENGTH`**: `14`
239
+ - **Erros**: veja acima (`CnpjDV::Error`, `DomainError` e folhas levantadas)
240
+
241
+ ## Algoritmo de cΓ‘lculo
242
+
243
+ O pacote calcula os dΓ­gitos verificadores com as regras oficiais brasileiras de mΓ³dulo 11 estendidas a caracteres alfanumΓ©ricos:
244
+
245
+ 1. **Valor do caractere:** cada caractere contribui com `ord(caractere) βˆ’ 48` (assim `0`–`9` permanecem 0–9; letras usam o deslocamento ASCII em relaΓ§Γ£o a `0`).
246
+ 2. **Pesos:** da **direita para a esquerda**, multiplicar pelos pesos que ciclam **2, 3, 4, 5, 6, 7, 8, 9** e voltam a 2.
247
+ 3. **Primeiro dΓ­gito verificador (13Βͺ posiΓ§Γ£o):** aplicar os itens 1–2 aos **primeiros 12** caracteres da base; seja `r = soma % 11`. O dΓ­gito Γ© `0` se `r < 2`, senΓ£o `11 βˆ’ r`.
248
+ 4. **Segundo dΓ­gito verificador (14Βͺ posiΓ§Γ£o):** aplicar os itens 1–2 aos 12 primeiros caracteres **mais** o primeiro dΓ­gito verificador; mesma fΓ³rmula para `r`.
249
+
250
+ ## ContribuiΓ§Γ£o e suporte
251
+
252
+ ContribuiΓ§Γ΅es sΓ£o bem-vindas! Consulte as [Diretrizes de contribuiΓ§Γ£o](https://github.com/LacusSolutions/br-utils-ruby/blob/main/CONTRIBUTING.md). Se o projeto for ΓΊtil para vocΓͺ, considere:
253
+
254
+ - ⭐ Dar uma estrela no repositório
255
+ - 🀝 Contribuir com código
256
+ - πŸ’‘ [Sugerir novas funcionalidades](https://github.com/LacusSolutions/br-utils-ruby/issues)
257
+ - πŸ› [Reportar bugs](https://github.com/LacusSolutions/br-utils-ruby/issues)
258
+
259
+ ## LicenΓ§a
260
+
261
+ Este projeto estΓ‘ sob a licenΓ§a MIT β€” veja o arquivo [LICENSE](https://github.com/LacusSolutions/br-utils-ruby/blob/main/LICENSE).
262
+
263
+ ## Changelog
264
+
265
+ Veja o [CHANGELOG](./CHANGELOG.md) para alteraΓ§Γ΅es e histΓ³rico de versΓ΅es.
266
+
267
+ ---
268
+
269
+ Feito com ❀️ por [Lacus Solutions](https://github.com/LacusSolutions)
@@ -0,0 +1,235 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'errors'
4
+
5
+ module CnpjDV
6
+ # Minimum number of characters required for the CNPJ check digits calculation.
7
+ CNPJ_MIN_LENGTH = 12
8
+
9
+ # Maximum number of characters accepted as input for the CNPJ check digits
10
+ # calculation.
11
+ CNPJ_MAX_LENGTH = 14
12
+
13
+ CNPJ_BASE_ID_LENGTH = 8
14
+ CNPJ_INVALID_BASE_ID = '0' * CNPJ_BASE_ID_LENGTH
15
+ CNPJ_BRANCH_ID_LENGTH = 4
16
+ CNPJ_INVALID_BRANCH_ID = '0' * CNPJ_BRANCH_ID_LENGTH
17
+
18
+ DELTA_FACTOR = '0'.ord
19
+ WEIGHTS = [2, 3, 4, 5, 6, 7, 8, 9].freeze
20
+ DIGIT_CHARS = '0123456789'
21
+
22
+ private_constant :CNPJ_BASE_ID_LENGTH, :CNPJ_INVALID_BASE_ID,
23
+ :CNPJ_BRANCH_ID_LENGTH, :CNPJ_INVALID_BRANCH_ID,
24
+ :DELTA_FACTOR, :WEIGHTS, :DIGIT_CHARS
25
+
26
+ # Calculates and exposes CNPJ check digits from a valid base input.
27
+ #
28
+ # Validates length, base ID, branch ID and rejects repeated numeric digits.
29
+ #
30
+ # @example Numeric base
31
+ # check_digits = CnpjDV::CnpjCheckDigits.new('914157320007')
32
+ # check_digits.first # => '9'
33
+ # check_digits.second # => '3'
34
+ # check_digits.both # => '93'
35
+ # check_digits.cnpj # => '91415732000793'
36
+ #
37
+ # @example Alphanumeric base
38
+ # check_digits = CnpjDV::CnpjCheckDigits.new('MGKGMJ9X0001')
39
+ # check_digits.first # => '6'
40
+ # check_digits.second # => '8'
41
+ # check_digits.both # => '68'
42
+ # check_digits.cnpj # => 'MGKGMJ9X000168'
43
+ class CnpjCheckDigits
44
+ # Creates a calculator for the given CNPJ base (12 to 14 characters).
45
+ #
46
+ # @param cnpj_input [String, Array<String>] alphanumeric CNPJ with or without
47
+ # formatting, or an array of strings
48
+ # @raise [TypeMismatchError] when input is not a +String+ or +Array<String>+
49
+ # @raise [InvalidLengthError] when character count is not between 12 and 14
50
+ # @raise [ValidationError] when base ID is all zero (+00.000.000+), branch ID
51
+ # is all zero (+0000+) or all digits are numeric the same (repeated digits,
52
+ # e.g. +77.777.777/7777-...+)
53
+ def initialize(cnpj_input)
54
+ parsed_input = parse_input(cnpj_input)
55
+
56
+ validate_length(parsed_input, cnpj_input)
57
+ validate_base_id(parsed_input, cnpj_input)
58
+ validate_branch_id(parsed_input, cnpj_input)
59
+ validate_non_repeated_digits(parsed_input, cnpj_input)
60
+
61
+ @cnpj_chars = parsed_input[0, CNPJ_MIN_LENGTH]
62
+ @cached_first_digit = nil
63
+ @cached_second_digit = nil
64
+ end
65
+
66
+ # First check digit (13th character of the full CNPJ).
67
+ #
68
+ # @return [String] a single numeric character (+"0"+–+"9"+)
69
+ def first
70
+ @cached_first_digit = _calculate(@cnpj_chars) if @cached_first_digit.nil?
71
+
72
+ DIGIT_CHARS[@cached_first_digit]
73
+ end
74
+
75
+ # Second check digit (14th character of the full CNPJ).
76
+ #
77
+ # @return [String] a single numeric character (+"0"+–+"9"+)
78
+ def second
79
+ @cached_second_digit = _calculate([*@cnpj_chars, first]) if @cached_second_digit.nil?
80
+
81
+ DIGIT_CHARS[@cached_second_digit]
82
+ end
83
+
84
+ # Both check digits concatenated (13th and 14th characters).
85
+ #
86
+ # @return [String] two-character numeric string
87
+ def both
88
+ first + second
89
+ end
90
+
91
+ # Full 14-character CNPJ (base 12 characters concatenated with the 2 check
92
+ # digits).
93
+ #
94
+ # @return [String] 14-character CNPJ (base may contain letters + numeric DVs)
95
+ def cnpj
96
+ @cnpj_chars.join + both
97
+ end
98
+
99
+ # Protected (not private) so test spy subclasses can override and call +super+.
100
+ # Leading underscore matches the cross-language helper name (`_calculate`).
101
+ protected
102
+
103
+ # Computes a single check digit using the standard CNPJ modulo-11 algorithm.
104
+ #
105
+ # @param cnpj_sequence [Array<String>] characters used in the weighted sum
106
+ # @return [Integer] check digit in the range 0–9
107
+ def _calculate(cnpj_sequence)
108
+ length = cnpj_sequence.length
109
+ sum_result = 0
110
+
111
+ (length - 1).downto(0) do |index|
112
+ char_value = cnpj_sequence[index].ord - DELTA_FACTOR
113
+ sum_result += char_value * WEIGHTS[(length - 1 - index) % 8]
114
+ end
115
+
116
+ remainder = sum_result % 11
117
+
118
+ remainder < 2 ? 0 : 11 - remainder
119
+ end
120
+
121
+ private
122
+
123
+ # Parses a string or an array of strings into alphanumeric characters.
124
+ #
125
+ # @param cnpj_input [Object] candidate CNPJ input
126
+ # @return [Array<String>] uppercase alphanumeric characters
127
+ # @raise [TypeMismatchError] when input is not a +String+ or +Array<String>+
128
+ def parse_input(cnpj_input)
129
+ return parse_string_input(cnpj_input) if cnpj_input.is_a?(String)
130
+ return parse_array_input(cnpj_input) if cnpj_input.is_a?(Array)
131
+
132
+ raise TypeMismatchError.new(cnpj_input, 'string or string[]')
133
+ end
134
+
135
+ # Strips non-alphanumeric characters and uppercases the remainder.
136
+ #
137
+ # @param cnpj_string [String] raw or formatted CNPJ string
138
+ # @return [Array<String>] uppercase alphanumeric characters
139
+ def parse_string_input(cnpj_string)
140
+ result = []
141
+
142
+ cnpj_string.each_char do |char|
143
+ code = char.ord
144
+ if code.between?(48, 57) || code.between?(65, 90)
145
+ result << char
146
+ elsif code.between?(97, 122)
147
+ result << (code - 32).chr
148
+ end
149
+ end
150
+
151
+ result
152
+ end
153
+
154
+ # Concatenates an array of strings and normalizes the result.
155
+ #
156
+ # @param cnpj_array [Array] candidate array of string chunks
157
+ # @return [Array<String>] uppercase alphanumeric characters
158
+ # @raise [TypeMismatchError] when input is not a +String+ or +Array<String>+
159
+ def parse_array_input(cnpj_array)
160
+ return [] if cnpj_array.empty?
161
+
162
+ raise TypeMismatchError.new(cnpj_array, 'string or string[]') unless cnpj_array.all?(String)
163
+
164
+ parse_string_input(cnpj_array.join)
165
+ end
166
+
167
+ # Ensures character count is between {CNPJ_MIN_LENGTH} and
168
+ # {CNPJ_MAX_LENGTH}.
169
+ #
170
+ # @param cnpj_chars [Array<String>] normalized characters
171
+ # @param original_input [String, Array<String>] original caller input
172
+ # @raise [InvalidLengthError] when character count is not between 12 and 14
173
+ def validate_length(cnpj_chars, original_input)
174
+ chars_count = cnpj_chars.length
175
+
176
+ return if chars_count.between?(CNPJ_MIN_LENGTH, CNPJ_MAX_LENGTH)
177
+
178
+ raise InvalidLengthError.new(
179
+ original_input,
180
+ cnpj_chars.join,
181
+ CNPJ_MIN_LENGTH,
182
+ CNPJ_MAX_LENGTH
183
+ )
184
+ end
185
+
186
+ # Rejects base ID (first 8 digits) when it is all zeros.
187
+ #
188
+ # @param cnpj_chars [Array<String>] normalized characters
189
+ # @param original_input [String, Array<String>] original caller input
190
+ # @raise [ValidationError] when base ID is all zeros (+00.000.000+)
191
+ def validate_base_id(cnpj_chars, original_input)
192
+ return unless cnpj_chars[0, CNPJ_BASE_ID_LENGTH].all? { |char| char == '0' }
193
+
194
+ raise ValidationError.new(
195
+ original_input,
196
+ "Base ID \"#{CNPJ_INVALID_BASE_ID}\" is not eligible."
197
+ )
198
+ end
199
+
200
+ # Rejects branch ID (digits 9–12) when it is all zeros.
201
+ #
202
+ # @param cnpj_chars [Array<String>] normalized characters
203
+ # @param original_input [String, Array<String>] original caller input
204
+ # @raise [ValidationError] when branch ID is all zeros (+0000+)
205
+ def validate_branch_id(cnpj_chars, original_input)
206
+ branch_start = CNPJ_BASE_ID_LENGTH
207
+ branch_end = branch_start + CNPJ_BRANCH_ID_LENGTH
208
+ branch_id = cnpj_chars[branch_start...branch_end]
209
+
210
+ return unless branch_id.all? { |char| char == '0' }
211
+
212
+ raise ValidationError.new(
213
+ original_input,
214
+ "Branch ID \"#{CNPJ_INVALID_BRANCH_ID}\" is not eligible."
215
+ )
216
+ end
217
+
218
+ # Rejects inputs where all first 12 characters are the same numeric digit.
219
+ #
220
+ # @param cnpj_chars [Array<String>] normalized characters
221
+ # @param original_input [String, Array<String>] original caller input
222
+ # @raise [ValidationError] when all digits are numeric the same (repeated
223
+ # digits, e.g. +77.777.777/7777-...+)
224
+ def validate_non_repeated_digits(cnpj_chars, original_input)
225
+ first_char = cnpj_chars[0]
226
+ return unless first_char.match?(/\A\d\z/)
227
+ return unless cnpj_chars[1, CNPJ_MIN_LENGTH - 1].all? { |char| char == first_char }
228
+
229
+ raise ValidationError.new(
230
+ original_input,
231
+ 'Repeated digits are not considered valid.'
232
+ )
233
+ end
234
+ end
235
+ end
@@ -0,0 +1,122 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'json'
4
+ require 'lacus-utils'
5
+
6
+ module CnpjDV
7
+ # Formats the original input for inclusion in an exception message.
8
+ module FormatActualInput
9
+ module_function
10
+
11
+ # @param actual_input [Object] the original input value
12
+ # @return [String] a quoted string, or compact JSON for arrays
13
+ def call(actual_input)
14
+ return "\"#{actual_input}\"" if actual_input.is_a?(String)
15
+
16
+ JSON.generate(actual_input)
17
+ end
18
+ end
19
+ private_constant :FormatActualInput
20
+
21
+ # Marker module mixed into every custom error raised by this library.
22
+ #
23
+ # Use +rescue CnpjDV::Error+ to catch every library error regardless of native
24
+ # ancestry.
25
+ module Error; end
26
+
27
+ # API misuse error raised when an argument's runtime type does not match the
28
+ # type required by the API contract.
29
+ class TypeMismatchError < TypeError
30
+ include Error
31
+
32
+ # @return [Object] the offending input value
33
+ attr_reader :actual_input
34
+
35
+ # @return [String] human-readable type of {#actual_input}
36
+ attr_reader :actual_type
37
+
38
+ # @return [String] description of the expected type
39
+ attr_reader :expected_type
40
+
41
+ # @param actual_input [Object] the offending input value (the whole array
42
+ # when a non-string element is found)
43
+ # @param expected_type [String] description of the expected type (e.g.
44
+ # +"string or string[]"+)
45
+ def initialize(actual_input, expected_type)
46
+ actual_type = LacusUtils.describe_type(actual_input)
47
+
48
+ super("CNPJ input must be of type #{expected_type}. Got #{actual_type}.")
49
+ @actual_input = actual_input
50
+ @actual_type = actual_type
51
+ @expected_type = expected_type
52
+ end
53
+ end
54
+
55
+ # Domain error ancestor for business-rule failures (length, validation, and
56
+ # other domain leaves). Prefer raising a leaf subclass.
57
+ class DomainError < RangeError
58
+ include Error
59
+ end
60
+
61
+ # Domain error raised when a string, array, or other collection has a length
62
+ # outside the bounds required by the domain rule.
63
+ class InvalidLengthError < DomainError
64
+ # @return [String, Array<String>] the original input
65
+ attr_reader :actual_input
66
+
67
+ # @return [String] the normalized alphanumeric string
68
+ attr_reader :evaluated_input
69
+
70
+ # @return [Integer] minimum expected length (12)
71
+ attr_reader :min_expected_length
72
+
73
+ # @return [Integer] maximum expected length (14)
74
+ attr_reader :max_expected_length
75
+
76
+ # @param actual_input [String, Array<String>] the original input
77
+ # @param evaluated_input [String] the normalized alphanumeric string
78
+ # @param min_expected_length [Integer] minimum expected length
79
+ # @param max_expected_length [Integer] maximum expected length
80
+ def initialize(actual_input, evaluated_input, min_expected_length, max_expected_length)
81
+ super(build_message(actual_input, evaluated_input, min_expected_length, max_expected_length))
82
+ @actual_input = actual_input
83
+ @evaluated_input = evaluated_input
84
+ @min_expected_length = min_expected_length
85
+ @max_expected_length = max_expected_length
86
+ end
87
+
88
+ private
89
+
90
+ def build_message(actual_input, evaluated_input, min_len, max_len)
91
+ fmt_actual = FormatActualInput.call(actual_input)
92
+ fmt_evaluated =
93
+ if actual_input == evaluated_input
94
+ evaluated_input.length.to_s
95
+ else
96
+ "#{evaluated_input.length} in \"#{evaluated_input}\""
97
+ end
98
+
99
+ "CNPJ input #{fmt_actual} does not contain #{min_len} to #{max_len} characters. " \
100
+ "Got #{fmt_evaluated}."
101
+ end
102
+ end
103
+
104
+ # Domain error raised when a value has a valid type and length but violates a
105
+ # validation rule that is not numeric-range or length-based (e.g. ineligible
106
+ # base/branch ID or repeated numeric digits).
107
+ class ValidationError < DomainError
108
+ # @return [String, Array<String>] the original input
109
+ attr_reader :actual_input
110
+
111
+ # @return [String] human-readable reason why the input is invalid
112
+ attr_reader :reason
113
+
114
+ # @param actual_input [String, Array<String>] the original input
115
+ # @param reason [String] human-readable reason why the input is invalid
116
+ def initialize(actual_input, reason)
117
+ super("CNPJ input #{FormatActualInput.call(actual_input)} is invalid. #{reason}")
118
+ @actual_input = actual_input
119
+ @reason = reason
120
+ end
121
+ end
122
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- module CnpjDv
4
- VERSION = '0.0.1'
3
+ module CnpjDV
4
+ VERSION = '2.0.0'
5
5
  end
data/src/cnpj-dv.rb CHANGED
@@ -1,9 +1,35 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require_relative 'cnpj-dv/version'
4
+ require_relative 'cnpj-dv/errors'
5
+ require_relative 'cnpj-dv/cnpj_check_digits'
4
6
 
5
- module CnpjDv
6
- def self.hello
7
- 'cnpj-dv'
8
- end
7
+ # Check-digit calculation for Brazilian CNPJ (numeric and alphanumeric formats).
8
+ #
9
+ # Errors fall into two categories:
10
+ #
11
+ # - *API misuse* β€” the caller invoked the library incorrectly (wrong type).
12
+ # Raised as {CnpjDV::TypeMismatchError} (+TypeError+).
13
+ # - *Domain errors* β€” the call shape was valid, but a value violates a business
14
+ # rule (invalid length, ineligible base/branch, repeated digits). Length
15
+ # failures raise {CnpjDV::InvalidLengthError}; other domain failures raise
16
+ # {CnpjDV::ValidationError} (both under {CnpjDV::DomainError} / +RangeError+).
17
+ #
18
+ # Every custom error includes the {CnpjDV::Error} marker module so consumers can
19
+ # +rescue CnpjDV::Error+ for a library-wide catch.
20
+ #
21
+ # Public API:
22
+ #
23
+ # - {CnpjDV::CnpjCheckDigits}
24
+ # - {CnpjDV::CNPJ_MIN_LENGTH}, {CnpjDV::CNPJ_MAX_LENGTH}
25
+ # - Error marker {CnpjDV::Error}; domain ancestor {CnpjDV::DomainError};
26
+ # raised leaves {CnpjDV::TypeMismatchError}, {CnpjDV::InvalidLengthError},
27
+ # {CnpjDV::ValidationError}
28
+ #
29
+ # @example
30
+ # require 'cnpj-dv'
31
+ #
32
+ # check_digits = CnpjDV::CnpjCheckDigits.new('914157320007')
33
+ # check_digits.cnpj # => '91415732000793'
34
+ module CnpjDV
9
35
  end
metadata CHANGED
@@ -1,28 +1,55 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: cnpj-dv
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.1
4
+ version: 2.0.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Julio L. Muller
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2026-07-07 00:00:00.000000000 Z
12
- dependencies: []
13
- description:
11
+ date: 2026-07-20 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: lacus-utils
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: 1.1.0
20
+ - - "<"
21
+ - !ruby/object:Gem::Version
22
+ version: 2.0.0
23
+ type: :runtime
24
+ prerelease: false
25
+ version_requirements: !ruby/object:Gem::Requirement
26
+ requirements:
27
+ - - ">="
28
+ - !ruby/object:Gem::Version
29
+ version: 1.1.0
30
+ - - "<"
31
+ - !ruby/object:Gem::Version
32
+ version: 2.0.0
33
+ description: Utility to calculate check digits on CNPJ (Brazilian Business Tax ID)
14
34
  email:
35
+ - juliolmuller@outlook.com
15
36
  executables: []
16
37
  extensions: []
17
38
  extra_rdoc_files: []
18
39
  files:
40
+ - CHANGELOG.md
19
41
  - LICENSE
42
+ - README.md
43
+ - README.pt.md
20
44
  - src/cnpj-dv.rb
45
+ - src/cnpj-dv/cnpj_check_digits.rb
46
+ - src/cnpj-dv/errors.rb
21
47
  - src/cnpj-dv/version.rb
22
48
  homepage: https://github.com/LacusSolutions/br-utils-ruby
23
49
  licenses:
24
50
  - MIT
25
51
  metadata:
52
+ source_code_uri: https://github.com/LacusSolutions/br-utils-ruby
26
53
  rubygems_mfa_required: 'true'
27
54
  post_install_message:
28
55
  rdoc_options: []
@@ -32,7 +59,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
32
59
  requirements:
33
60
  - - ">="
34
61
  - !ruby/object:Gem::Version
35
- version: '3.2'
62
+ version: '3.1'
36
63
  required_rubygems_version: !ruby/object:Gem::Requirement
37
64
  requirements:
38
65
  - - ">="
@@ -42,5 +69,5 @@ requirements: []
42
69
  rubygems_version: 3.4.19
43
70
  signing_key:
44
71
  specification_version: 4
45
- summary: Check-digit calculation for CNPJ (Brazilian company ID)
72
+ summary: Check-digit calculation for CNPJ (Brazilian Business Tax ID)
46
73
  test_files: []