From c6252d6502fa3241e73664a29e5f26641ab7804d Mon Sep 17 00:00:00 2001 From: Luciano Medeiros Date: Tue, 14 Jan 2025 08:29:22 -0300 Subject: [PATCH] feat: Add NUP formatter and validator Adds `NUPInputFormatter` to format the NUP, adds `NUPValidator` to validate and format the NUP, and adds `obterNUP` and `isNUPValido` to `UtilBrasilFields`. Also adds a new test file for the NUP formatter. --- README.md | 49 ++++++++------- example/lib/main.dart | 4 ++ lib/brasil_fields.dart | 1 + lib/src/formatters/nup_input_formatter.dart | 46 ++++++++++++++ lib/src/util/util_brasil_fields.dart | 10 +++ lib/src/validators/nup_validator.dart | 52 +++++++++++++++ test/brasil_fields_test.dart | 9 +++ test/nup_test.dart | 28 +++++++++ .../formatters/nup_input_formatter_test.dart | 63 +++++++++++++++++++ test/util_brasil_fields_test.dart | 11 ++++ 10 files changed, 250 insertions(+), 23 deletions(-) create mode 100644 lib/src/formatters/nup_input_formatter.dart create mode 100644 lib/src/validators/nup_validator.dart create mode 100644 test/nup_test.dart create mode 100644 test/src/formatters/nup_input_formatter_test.dart diff --git a/README.md b/README.md index ae35c23..53944bb 100644 --- a/README.md +++ b/README.md @@ -30,29 +30,30 @@ TextFormField( ### Formatters -| Padrão | Formatter | Formato -|:------------------|:-------------------------------|:---------------- -| Altura | AlturaInputFormatter() | 2,22 -| Cartão | CartaoBancarioInputFormatter() | 0000 1111 2222 3333 4444 -| Centavos | CentavosInputFormatter() | 7,194 -| CEP | CepInputFormatter() | 99.999-999 -| CPF | CpfInputFormatter() | 999.999.99-99 -| CNPJ | CnpjInputFormatter() | 99.999.999/9999-99 -| CPF / CNPJ | CpfOuCnpjFormatter() | se adapta conforme os números são inseridos -| CEST | CESTInputFormatter() | 12.345.67 -| CNS | CNSInputFormatter() | 111 2222 3333 4444 -| Data | DataInputFormatter() | 01/01/1900 -| Hora | HoraInputFormatter() | 23:59 -| IOF | HoraInputFormatter() | 1,234567 -| KM | KmInputFormatter() | 999.999 -| Cert. nascimento | CertNascimentoInputFormatter() | 000000 11 22 3333 4 55555 666 7777777 88 -| NCM | NCMInputFormatter() | 1234.56.78 -| Peso | PesoInputFormatter() | 111,1 -| Placa | PlacaVeiculoInputFormatter() | AAA-1234 (**não** utilizar `FilteringTextInputFormatter.digitsOnly`) -| Real | RealInputFormatter() | 20.550 -| Telefone | TelefoneInputFormatter() | (99) 9999-9999 -| Validade cartão | ValidadeCartaoInputFormatter() | 12/24 ou 12/2024 -| Temperatura | TemperaturaInputFormatter() |27,1 +| Padrão | Formatter | Formato | +|:-----------------|:-------------------------------|:---------------------------------------------------------------------| +| Altura | AlturaInputFormatter() | 2,22 | +| Cartão | CartaoBancarioInputFormatter() | 0000 1111 2222 3333 4444 | +| Centavos | CentavosInputFormatter() | 7,194 | +| CEP | CepInputFormatter() | 99.999-999 | +| CPF | CpfInputFormatter() | 999.999.99-99 | +| CNPJ | CnpjInputFormatter() | 99.999.999/9999-99 | +| CPF / CNPJ | CpfOuCnpjFormatter() | se adapta conforme os números são inseridos | +| CEST | CESTInputFormatter() | 12.345.67 | +| CNS | CNSInputFormatter() | 111 2222 3333 4444 | +| Data | DataInputFormatter() | 01/01/1900 | +| Hora | HoraInputFormatter() | 23:59 | +| IOF | HoraInputFormatter() | 1,234567 | +| KM | KmInputFormatter() | 999.999 | +| Cert. nascimento | CertNascimentoInputFormatter() | 000000 11 22 3333 4 55555 666 7777777 88 | +| NCM | NCMInputFormatter() | 1234.56.78 | +| NUP | NUPInputFormatter() | 1234567-89.0123.4.56.7890 | +| Peso | PesoInputFormatter() | 111,1 | +| Placa | PlacaVeiculoInputFormatter() | AAA-1234 (**não** utilizar `FilteringTextInputFormatter.digitsOnly`) | +| Real | RealInputFormatter() | 20.550 | +| Telefone | TelefoneInputFormatter() | (99) 9999-9999 | +| Validade cartão | ValidadeCartaoInputFormatter() | 12/24 ou 12/2024 | +| Temperatura | TemperaturaInputFormatter() | 27,1 | ### Modelos @@ -97,6 +98,7 @@ Métodos que facilitam manipular valores: - `UtilBrasilFields.obterCnpj('11222333444455')` (11.222.333/4444-55) - `UtilBrasilFields.obterCep('11222333')` (11.222-333) - `UtilBrasilFields.obterCep('11222333', ponto: false)` (11222-333) +- `UtilBrasilFields.obterNUP('06010642120226000000')` (0601064-21.2022.6.00.0000) - `UtilBrasilFields.obterTelefone('00999998877')` ((00) 99999-8877) - `UtilBrasilFields.obterTelefone('(00) 99999-8877', mascara: false)` (00999998877) - `UtilBrasilFields.obterTelefone('999998877', ddd: false)` (99999-8877) @@ -112,6 +114,7 @@ Métodos que facilitam manipular valores: - `UtilBrasilFields.converterMoedaParaDouble` (remove o R$ e retorna um double) - `UtilBrasilFields.isCPFValido` (retorna `true` se o CPF for válido, caso contrário, retorna `false`) - `UtilBrasilFields.isCNPJValido` (retorna `true` se o CNPJ for válido, caso contrário, retorna `false`) +- `UtilBrasilFields.isNUPValido` (retorna `true` se o NUP for válido, caso contrário, retorna `false`) Para inicializar um `TextEditingController` com o texto já formatado, basta escolher o método com o formato desejado e setar no atributo `text`: diff --git a/example/lib/main.dart b/example/lib/main.dart index a8bc09a..c85cd42 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -122,6 +122,10 @@ class MyApp extends StatelessWidget { label: 'NCM', formatter: NCMInputFormatter(), ), + RowFormatters( + label: 'NUP', + formatter: NUPInputFormatter(), + ), RowFormatters( label: 'CEST', formatter: CESTInputFormatter(), diff --git a/lib/brasil_fields.dart b/lib/brasil_fields.dart index 3acaab6..4fb122f 100644 --- a/lib/brasil_fields.dart +++ b/lib/brasil_fields.dart @@ -15,6 +15,7 @@ export 'src/formatters/hora_input_formatter.dart'; export 'src/formatters/iof_input_formatter.dart'; export 'src/formatters/km_input_formatter.dart'; export 'src/formatters/ncm_input_formatter.dart'; +export 'src/formatters/nup_input_formatter.dart'; export 'src/formatters/peso_input_formatter.dart'; export 'src/formatters/placa_veiculo_formatter.dart'; export 'src/formatters/real_input_formatter.dart'; diff --git a/lib/src/formatters/nup_input_formatter.dart b/lib/src/formatters/nup_input_formatter.dart new file mode 100644 index 0000000..225de75 --- /dev/null +++ b/lib/src/formatters/nup_input_formatter.dart @@ -0,0 +1,46 @@ +import 'package:flutter/services.dart'; + +/// Formata o valor do campo com a máscara de NUP (Numeração Única de Processos): `XXXXXXX-XX.XXXX.X.XX.XXXX` +/// Referência: [Documentação CNJ](https://www.cnj.jus.br/programas-e-acoes/numeracao-unica/) +class NUPInputFormatter extends TextInputFormatter { + @override + TextEditingValue formatEditUpdate( + TextEditingValue oldValue, TextEditingValue newValue) { + // verifica o tamanho máximo do campo + if (newValue.text.length > 20) return oldValue; + + var posicaoCursor = newValue.selection.end; + var substrIndex = 0; + final valorFinal = StringBuffer(); + + if (newValue.text.length >= 8) { + valorFinal.write('${newValue.text.substring(0, substrIndex = 7)}-'); + if (newValue.selection.end >= 7) posicaoCursor++; + } + if (newValue.text.length >= 10) { + valorFinal.write('${newValue.text.substring(7, substrIndex = 9)}.'); + if (newValue.selection.end >= 9) posicaoCursor++; + } + if (newValue.text.length >= 14) { + valorFinal.write('${newValue.text.substring(9, substrIndex = 13)}.'); + if (newValue.selection.end >= 13) posicaoCursor++; + } + if (newValue.text.length >= 15) { + valorFinal.write('${newValue.text.substring(13, substrIndex = 14)}.'); + if (newValue.selection.end >= 14) posicaoCursor++; + } + if (newValue.text.length >= 17) { + valorFinal.write('${newValue.text.substring(14, substrIndex = 16)}.'); + if (newValue.selection.end >= 16) posicaoCursor++; + } + + if (newValue.text.length >= substrIndex) { + valorFinal.write(newValue.text.substring(substrIndex)); + } + + return TextEditingValue( + text: valorFinal.toString(), + selection: TextSelection.collapsed(offset: posicaoCursor), + ); + } +} diff --git a/lib/src/util/util_brasil_fields.dart b/lib/src/util/util_brasil_fields.dart index 8bdb909..6b648e5 100644 --- a/lib/src/util/util_brasil_fields.dart +++ b/lib/src/util/util_brasil_fields.dart @@ -1,4 +1,5 @@ import '../formatters/adiciona_separador.dart'; +import '../validators/nup_validator.dart'; import '../validators/validators.dart'; class UtilBrasilFields { @@ -76,6 +77,9 @@ class UtilBrasilFields { ///Faz a validação do CNPJ retornando `[true]` ou `[false]` static bool isCNPJValido(String? cnpj) => CNPJValidator.isValid(cnpj); + ///Faz a validação do NUP retornando `[true]` ou `[false]` + static bool isNUPValido(String? nup) => NUPValidator.isValid(nup); + /// Gera um CPF aleatório /// /// Formatado ou não formatado, baseado no parâmetro `useFormat`: @@ -139,6 +143,12 @@ class UtilBrasilFields { return CNPJValidator.strip(cnpj).substring(12); } + /// Retorna o NUP informado, utilizando a máscara: `NNNNNNN-DD.AAAA.J.TR.OOOO` + static String obterNUP(String nup) { + assert(isNUPValido(nup), 'Número de Processo inválido!'); + return NUPValidator.format(nup); + } + /// Retorna o número real informado, utilizando a máscara: `R$ 50.000,00` ou `50.000,00` static String obterReal(double value, {bool moeda = true, int decimal = 2}) { bool isNegative = false; diff --git a/lib/src/validators/nup_validator.dart b/lib/src/validators/nup_validator.dart new file mode 100644 index 0000000..c688889 --- /dev/null +++ b/lib/src/validators/nup_validator.dart @@ -0,0 +1,52 @@ +class NUPValidator { + static const stripRegex = r'[^\d]'; + + static bool isValid(String? nup, {stripBeforeValidation = true}) { + if (stripBeforeValidation) { + nup = strip(nup); + } + + if (nup == null || nup.isEmpty) { + return false; + } + + if (nup.length != 20) { + return false; + } + + final checkDigit = _checkDigit(nup); + return nup.substring(7, 9) == checkDigit.toString(); + } + + static String strip(String? nup) { + var regex = RegExp(stripRegex); + nup = nup ?? ''; + + return nup.replaceAll(regex, ''); + } + + // Compute the Check Digit (or 'Dígito Verificador (DV)' in PT-BR). + // You can learn more about the algorithm on [CNJ (pt-br)](https://atos.cnj.jus.br/files/compilado23285720221017634de539229ab.pdf) + static String _checkDigit(String nup) { + final sequential = nup.substring(0, 7); + final year = nup.substring(9, 13); + final segment = nup[13]; + final court = nup.substring(14, 16); + final origin = nup.substring(16); + + final r1 = int.parse(sequential) % 97; + final r2 = int.parse('$r1$year$segment$court') % 97; + final r3 = int.parse('$r2${origin}00') % 97; + + final checkDigit = 98 - r3; + return checkDigit.toString().padLeft(2, '0'); + } + + static String format(String nup) { + var regExp = RegExp(r'^(\d{7})(\d{2})(\d{4})(\d{1})(\d{2})(\d{4})$'); + + return strip(nup).replaceAllMapped( + regExp, (Match m) => '${m[1]}-${m[2]}.${m[3]}.${m[4]}.${m[5]}.${m[6]}'); + } + +} diff --git a/test/brasil_fields_test.dart b/test/brasil_fields_test.dart index cda4944..6b6577d 100644 --- a/test/brasil_fields_test.dart +++ b/test/brasil_fields_test.dart @@ -1,5 +1,6 @@ import 'package:brasil_fields/brasil_fields.dart'; import 'package:brasil_fields/src/formatters/compound_formatters/compound_formatter.dart'; +import 'package:brasil_fields/src/formatters/nup_input_formatter.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_test/flutter_test.dart'; @@ -333,6 +334,14 @@ void main() { expect(textController.text, '0309.90.00'); }); + testWidgets('NUPInputFormatter', (WidgetTester tester) async { + final textController = TextEditingController(); + + await tester.pumpWidget(boilerplate(NUPInputFormatter(), textController)); + await tester.enterText(find.byType(TextField), '12345678901234567890'); + expect(textController.text, '1234567-89.0123.4.56.7890'); + }); + testWidgets('CESTInputFormatter', (WidgetTester tester) async { final textController = TextEditingController(); diff --git a/test/nup_test.dart b/test/nup_test.dart new file mode 100644 index 0000000..ebb1faa --- /dev/null +++ b/test/nup_test.dart @@ -0,0 +1,28 @@ +import 'package:brasil_fields/src/validators/nup_validator.dart'; +import 'package:flutter_test/flutter_test.dart'; + +void main() { + test('Test NUP validator', () { + expect(NUPValidator.isValid('0601064-21.2022.6.00.0000'), true); + expect(NUPValidator.isValid('0601064-22.2022.6.00.0000'), false); + expect(NUPValidator.isValid('00601064-22.2022.6.00.0000'), false); + expect(NUPValidator.isValid('06010642120226000000'), true); + expect(NUPValidator.isValid('06010642220226000000'), false); + expect(NUPValidator.isValid('006010642120226000000'), false); + expect( + NUPValidator.isValid('03346teste1671002@mail', + stripBeforeValidation: false), + false); + expect( + NUPValidator.isValid('57abc803.6586-52', stripBeforeValidation: false), + false); + }); + + test('Test NUP formatter', () { + expect(NUPValidator.format('06010642120226000000'), '0601064-21.2022.6.00.0000'); + }); + + test('Test NUP strip', () { + expect(NUPValidator.strip('0601064-21.2022.6.00.0000'), '06010642120226000000'); + }); +} diff --git a/test/src/formatters/nup_input_formatter_test.dart b/test/src/formatters/nup_input_formatter_test.dart new file mode 100644 index 0000000..bd0e8d4 --- /dev/null +++ b/test/src/formatters/nup_input_formatter_test.dart @@ -0,0 +1,63 @@ +import 'package:brasil_fields/src/formatters/nup_input_formatter.dart'; +import 'package:flutter_test/flutter_test.dart'; + +void main() { + evaluate(String oldValue, String newValue) { + return NUPInputFormatter() + .formatEditUpdate( + TextEditingValue(text: oldValue), + TextEditingValue(text: newValue), + ) + .text; + } + + group('NUPInputFormatter', () { + test('padrao', () => expect(evaluate('', '12345678901234567890'), '1234567-89.0123.4.56.7890')); + test('limite 20 digitos', () => expect(evaluate('', '123456789012345678901'), '')); + test('backspace', () { + expect(evaluate('', '12345678901234567890'), '1234567-89.0123.4.56.7890'); + expect(evaluate('', '1234567890123456789'), '1234567-89.0123.4.56.789'); + expect(evaluate('', '123456789012345678'), '1234567-89.0123.4.56.78'); + expect(evaluate('', '12345678901234567'), '1234567-89.0123.4.56.7'); + expect(evaluate('', '1234567890123456'), '1234567-89.0123.4.56'); + expect(evaluate('', '123456789012345'), '1234567-89.0123.4.5'); + expect(evaluate('', '12345678901234'), '1234567-89.0123.4'); + expect(evaluate('', '1234567890123'), '1234567-89.0123'); + expect(evaluate('', '123456789012'), '1234567-89.012'); + expect(evaluate('', '12345678901'), '1234567-89.01'); + expect(evaluate('', '1234567890'), '1234567-89.0'); + expect(evaluate('', '123456789'), '1234567-89'); + expect(evaluate('', '12345678'), '1234567-8'); + expect(evaluate('', '1234567'), '1234567'); + expect(evaluate('', '123456'), '123456'); + expect(evaluate('', '12345'), '12345'); + expect(evaluate('', '1234'), '1234'); + expect(evaluate('', '123'), '123'); + expect(evaluate('', '12'), '12'); + expect(evaluate('', '1'), '1'); + }); + + test('digitacao', () { + expect(evaluate('', '1'), '1'); + expect(evaluate('', '12'), '12'); + expect(evaluate('', '123'), '123'); + expect(evaluate('', '1234'), '1234'); + expect(evaluate('', '12345'), '12345'); + expect(evaluate('', '123456'), '123456'); + expect(evaluate('', '1234567'), '1234567'); + expect(evaluate('', '12345678'), '1234567-8'); + expect(evaluate('', '123456789'), '1234567-89'); + expect(evaluate('', '1234567890'), '1234567-89.0'); + expect(evaluate('', '12345678901'), '1234567-89.01'); + expect(evaluate('', '123456789012'), '1234567-89.012'); + expect(evaluate('', '1234567890123'), '1234567-89.0123'); + expect(evaluate('', '12345678901234'), '1234567-89.0123.4'); + expect(evaluate('', '123456789012345'), '1234567-89.0123.4.5'); + expect(evaluate('', '1234567890123456'), '1234567-89.0123.4.56'); + expect(evaluate('', '12345678901234567'), '1234567-89.0123.4.56.7'); + expect(evaluate('', '123456789012345678'), '1234567-89.0123.4.56.78'); + expect(evaluate('', '1234567890123456789'), '1234567-89.0123.4.56.789'); + expect(evaluate('', '12345678901234567890'), '1234567-89.0123.4.56.7890'); + }); + }); +} diff --git a/test/util_brasil_fields_test.dart b/test/util_brasil_fields_test.dart index 8109b53..3b284eb 100644 --- a/test/util_brasil_fields_test.dart +++ b/test/util_brasil_fields_test.dart @@ -20,6 +20,11 @@ void main() { expect(UtilBrasilFields.removeCaracteres(cep), '11222333'); }); + test('NUP', () { + const nup = '0601064-21.2022.6.00.0000'; + expect(UtilBrasilFields.removeCaracteres(nup), '06010642120226000000'); + }); + test('Real', () { const real = '11.222'; expect(UtilBrasilFields.removeCaracteres(real), '11222'); @@ -263,6 +268,12 @@ void main() { expect(UtilBrasilFields.obterCnpjDiv(cpnjComMascara), '90'); }); + test('Obter NUP', () { + const nupSemMascara = '06010642120226000000'; + const nupComMascara = '0601064-21.2022.6.00.0000'; + expect(UtilBrasilFields.obterNUP(nupSemMascara), nupComMascara); + }); + group('Obter Real', () { test('com moeda (R\$)', () { const real = 85437107.04;