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 +4 -4
- data/CHANGELOG.md +30 -0
- data/README.md +306 -0
- data/README.pt.md +269 -0
- data/src/cnpj-dv/cnpj_check_digits.rb +235 -0
- data/src/cnpj-dv/errors.rb +122 -0
- data/src/cnpj-dv/version.rb +2 -2
- data/src/cnpj-dv.rb +30 -4
- metadata +33 -6
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 8d6109a8bd760c61186dc99ec5503bb42880d9ebb3c19745962ba5a0d97c6cfc
|
|
4
|
+
data.tar.gz: 47242e1f246d23ebab4d8c3be97d7d7b46060c974e011ad0412a93595fb69767
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
|
+

|
|
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
|
data/src/cnpj-dv/version.rb
CHANGED
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
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
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
|
|
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-
|
|
12
|
-
dependencies:
|
|
13
|
-
|
|
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.
|
|
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
|
|
72
|
+
summary: Check-digit calculation for CNPJ (Brazilian Business Tax ID)
|
|
46
73
|
test_files: []
|